diff --git a/article-api/src/main/scala/no/ndla/articleapi/ComponentRegistry.scala b/article-api/src/main/scala/no/ndla/articleapi/ComponentRegistry.scala index d996317453..92c6dd794b 100644 --- a/article-api/src/main/scala/no/ndla/articleapi/ComponentRegistry.scala +++ b/article-api/src/main/scala/no/ndla/articleapi/ComponentRegistry.scala @@ -31,7 +31,6 @@ import no.ndla.network.tapir.{ TapirHealthController } import no.ndla.network.clients.{FeideApiClient, RedisClient} -import no.ndla.network.scalatra.{NdlaControllerBase, NdlaSwaggerSupport} import no.ndla.search.{BaseIndexService, Elastic4sClient} class ComponentRegistry(properties: ArticleApiProperties) @@ -40,8 +39,6 @@ class ComponentRegistry(properties: ArticleApiProperties) with DataSource with InternController with ArticleControllerV2 - with NdlaControllerBase - with NdlaSwaggerSupport with ArticleRepository with Elastic4sClient with SearchApiClient diff --git a/article-api/src/main/scala/no/ndla/articleapi/model/domain/Cachable.scala b/article-api/src/main/scala/no/ndla/articleapi/model/domain/Cachable.scala index 0fa786f741..415cf38b35 100644 --- a/article-api/src/main/scala/no/ndla/articleapi/model/domain/Cachable.scala +++ b/article-api/src/main/scala/no/ndla/articleapi/model/domain/Cachable.scala @@ -18,7 +18,7 @@ import scala.util.Try * that require login * * One would use the class by using `Cachable.yes(value)` (Or `Cachable.no(value)`) for values that can be cached and - * then use `returnValue.Ok()` in the controller to get the scalatra type with headers. + * then use `returnValue.Ok()` in the controller to get the resutling type with headers in a tuple. */ case class Cachable[T]( value: T, diff --git a/article-api/src/test/scala/no/ndla/articleapi/TestEnvironment.scala b/article-api/src/test/scala/no/ndla/articleapi/TestEnvironment.scala index 0b31696616..0dd790aac5 100644 --- a/article-api/src/test/scala/no/ndla/articleapi/TestEnvironment.scala +++ b/article-api/src/test/scala/no/ndla/articleapi/TestEnvironment.scala @@ -22,7 +22,6 @@ import no.ndla.articleapi.model.domain.DBArticle import no.ndla.common.Clock import no.ndla.network.NdlaClient import no.ndla.network.clients.{FeideApiClient, RedisClient} -import no.ndla.network.scalatra.{NdlaControllerBase, NdlaSwaggerSupport} import no.ndla.network.tapir.{NdlaMiddleware, Routes, Service, TapirHealthController} import no.ndla.search.{BaseIndexService, Elastic4sClient} import org.mockito.scalatest.MockitoSugar @@ -34,8 +33,6 @@ trait TestEnvironment with IndexService with BaseIndexService with SearchService - with NdlaControllerBase - with NdlaSwaggerSupport with ArticleControllerV2 with InternController with NdlaMiddleware diff --git a/common/src/main/scala/no/ndla/common/errors/ValidationMessage.scala b/common/src/main/scala/no/ndla/common/errors/ValidationMessage.scala index aa432661f0..98e35bdf12 100644 --- a/common/src/main/scala/no/ndla/common/errors/ValidationMessage.scala +++ b/common/src/main/scala/no/ndla/common/errors/ValidationMessage.scala @@ -8,18 +8,14 @@ package no.ndla.common.errors -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty import sttp.tapir.Schema.annotations.description @description("A message describing a validation error on a specific field") -@ApiModel(description = "A message describing a validation error on a specific field") +@description("A message describing a validation error on a specific field") case class ValidationMessage( @description("The field the error occured in") - @ApiModelProperty(description = "The field the error occured in") field: String, @description("The validation message") - @ApiModelProperty(description = "The validation message") message: String ) diff --git a/common/src/main/scala/no/ndla/common/model/api/DraftCopyright.scala b/common/src/main/scala/no/ndla/common/model/api/DraftCopyright.scala index 06e239a29c..ffffbc4be3 100644 --- a/common/src/main/scala/no/ndla/common/model/api/DraftCopyright.scala +++ b/common/src/main/scala/no/ndla/common/model/api/DraftCopyright.scala @@ -8,23 +8,21 @@ package no.ndla.common.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Description of copyright information") +@description("Description of copyright information") case class DraftCopyright( - @(ApiModelProperty @field)(description = "Describes the license of the article") license: Option[License], - @(ApiModelProperty @field)(description = "Reference to where the article is procured") origin: Option[String], - @(ApiModelProperty @field)(description = "List of creators") creators: Seq[Author], - @(ApiModelProperty @field)(description = "List of processors") processors: Seq[Author], - @(ApiModelProperty @field)(description = "List of rightsholders") rightsholders: Seq[Author], - @(ApiModelProperty @field)(description = "Date from which the copyright is valid") validFrom: Option[NDLADate], - @(ApiModelProperty @field)(description = "Date to which the copyright is valid") validTo: Option[NDLADate], - @(ApiModelProperty @field)(description = "Whether or not the content has been processed") processed: Boolean + @description("Describes the license of the article") license: Option[License], + @description("Reference to where the article is procured") origin: Option[String], + @description("List of creators") creators: Seq[Author], + @description("List of processors") processors: Seq[Author], + @description("List of rightsholders") rightsholders: Seq[Author], + @description("Date from which the copyright is valid") validFrom: Option[NDLADate], + @description("Date to which the copyright is valid") validTo: Option[NDLADate], + @description("Whether or not the content has been processed") processed: Boolean ) object DraftCopyright { diff --git a/common/src/main/scala/no/ndla/common/model/api/RelatedContentLink.scala b/common/src/main/scala/no/ndla/common/model/api/RelatedContentLink.scala index 6dd763342e..9188604144 100644 --- a/common/src/main/scala/no/ndla/common/model/api/RelatedContentLink.scala +++ b/common/src/main/scala/no/ndla/common/model/api/RelatedContentLink.scala @@ -7,16 +7,14 @@ package no.ndla.common.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "External link related to the article") +@description("External link related to the article") case class RelatedContentLink( - @(ApiModelProperty @field)(description = "Title of the article") title: String, - @(ApiModelProperty @field)(description = "The url to where the article can be viewed") url: String + @description("Title of the article") title: String, + @description("The url to where the article can be viewed") url: String ) object RelatedContentLink { diff --git a/common/src/main/scala/no/ndla/common/model/api/draft/Comment.scala b/common/src/main/scala/no/ndla/common/model/api/draft/Comment.scala index de35d27d44..90cef40d42 100644 --- a/common/src/main/scala/no/ndla/common/model/api/draft/Comment.scala +++ b/common/src/main/scala/no/ndla/common/model/api/draft/Comment.scala @@ -10,18 +10,16 @@ package no.ndla.common.model.api.draft import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about a comment attached to an article") +@description("Information about a comment attached to an article") case class Comment( - @(ApiModelProperty @field)(description = "Id of the comment") id: String, - @(ApiModelProperty @field)(description = "Content of the comment") content: String, - @(ApiModelProperty @field)(description = "When the comment was created") created: NDLADate, - @(ApiModelProperty @field)(description = "When the comment was last updated") updated: NDLADate, - @(ApiModelProperty @field)(description = "If the comment is open or closed") isOpen: Boolean, - @(ApiModelProperty @field)(description = "If the comment is solved or not") solved: Boolean + @description("Id of the comment") id: String, + @description("Content of the comment") content: String, + @description("When the comment was created") created: NDLADate, + @description("When the comment was last updated") updated: NDLADate, + @description("If the comment is open or closed") isOpen: Boolean, + @description("If the comment is solved or not") solved: Boolean ) object Comment { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/ComponentRegistry.scala b/concept-api/src/main/scala/no/ndla/conceptapi/ComponentRegistry.scala index 88c4d44047..add0234289 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/ComponentRegistry.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/ComponentRegistry.scala @@ -21,7 +21,6 @@ import no.ndla.network.NdlaClient import no.ndla.search.{BaseIndexService, Elastic4sClient} import no.ndla.common.Clock import no.ndla.common.configuration.BaseComponentRegistry -import no.ndla.network.scalatra.{NdlaControllerBase, NdlaSwaggerSupport} import no.ndla.network.tapir.{ NdlaMiddleware, Routes, @@ -61,8 +60,6 @@ class ComponentRegistry(properties: ConceptApiProperties) with Props with DBMigrator with ErrorHelpers - with NdlaControllerBase - with NdlaSwaggerSupport with SearchSettingsHelper with DraftSearchSettingsHelper with TaxonomyApiClient diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/controller/DraftConceptController.scala b/concept-api/src/main/scala/no/ndla/conceptapi/controller/DraftConceptController.scala index 9a7426a707..b2240faffc 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/controller/DraftConceptController.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/controller/DraftConceptController.scala @@ -16,7 +16,6 @@ import no.ndla.conceptapi.service.search.{DraftConceptSearchService, SearchConve import no.ndla.conceptapi.service.{ConverterService, ReadService, WriteService} import no.ndla.conceptapi.{Eff, Props} import no.ndla.language.Language.AllLanguages -import no.ndla.network.scalatra.NdlaSwaggerSupport import no.ndla.network.tapir.NoNullJsonPrinter.jsonBody import no.ndla.network.tapir.TapirErrors.errorOutputsFor import no.ndla.network.tapir.auth.Permission.CONCEPT_API_WRITE @@ -37,7 +36,6 @@ trait DraftConceptController { with SearchConverterService with ConverterService with Props - with NdlaSwaggerSupport with ConceptControllerHelpers with ErrorHelpers => val draftConceptController: DraftConceptController diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/controller/PublishedConceptController.scala b/concept-api/src/main/scala/no/ndla/conceptapi/controller/PublishedConceptController.scala index 5634e926c4..0854d8306c 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/controller/PublishedConceptController.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/controller/PublishedConceptController.scala @@ -16,7 +16,6 @@ import no.ndla.conceptapi.service.search.{PublishedConceptSearchService, SearchC import no.ndla.conceptapi.service.{ReadService, WriteService} import no.ndla.conceptapi.{Eff, Props} import no.ndla.language.Language -import no.ndla.network.scalatra.NdlaSwaggerSupport import no.ndla.network.tapir.NoNullJsonPrinter.jsonBody import no.ndla.network.tapir.TapirErrors.errorOutputsFor import no.ndla.network.tapir.{DynamicHeaders, Service} @@ -33,7 +32,6 @@ trait PublishedConceptController { with PublishedConceptSearchService with SearchConverterService with Props - with NdlaSwaggerSupport with ConceptControllerHelpers with ErrorHelpers => val publishedConceptController: PublishedConceptController diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptContent.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptContent.scala index 01a32477de..89c0fce26d 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptContent.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptContent.scala @@ -9,15 +9,13 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about the concept") +@description("Information about the concept") case class ConceptContent( - @(ApiModelProperty @field)(description = "The content of this concept") content: String, - @(ApiModelProperty @field)(description = "The html content of this concept") htmlContent: String, - @(ApiModelProperty @field)(description = "The language of this concept") language: String + @description("The content of this concept") content: String, + @description("The html content of this concept") htmlContent: String, + @description("The language of this concept") language: String ) object ConceptContent { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptDomainDump.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptDomainDump.scala index 6b0553b1d5..4afa4307f7 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptDomainDump.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptDomainDump.scala @@ -7,19 +7,17 @@ package no.ndla.conceptapi.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import io.circe.{Decoder, Encoder} import no.ndla.conceptapi.model.domain +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Information about articles") +@description("Information about articles") case class ConceptDomainDump( - @(ApiModelProperty @field)(description = "The total number of concepts in the database") totalCount: Long, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The search results") results: Seq[domain.Concept] + @description("The total number of concepts in the database") totalCount: Long, + @description("For which page results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The search results") results: Seq[domain.Concept] ) object ConceptDomainDump { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptMetaImage.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptMetaImage.scala index 90d4307501..3ff343bd80 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptMetaImage.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptMetaImage.scala @@ -8,16 +8,14 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Meta image for the concept") +@description("Meta image for the concept") case class ConceptMetaImage( - @(ApiModelProperty @field)(description = "The meta image url") url: String, - @(ApiModelProperty @field)(description = "The alt text for the meta image") alt: String, - @(ApiModelProperty @field)( - description = "The ISO 639-1 language code describing which concept translation this meta image belongs to" + @description("The meta image url") url: String, + @description("The alt text for the meta image") alt: String, + @description( + "The ISO 639-1 language code describing which concept translation this meta image belongs to" ) language: String ) diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptResponsible.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptResponsible.scala index 83fd4c6eb2..041090ce9c 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptResponsible.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptResponsible.scala @@ -8,17 +8,13 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field import no.ndla.common.model.NDLADate +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Information about the responsible") +@description("Information about the responsible") case class ConceptResponsible( - // format: off - @(ApiModelProperty @field)(description = "NDLA ID of responsible editor") responsibleId: String, - @(ApiModelProperty @field)(description = "Date of when the responsible editor was last updated") lastUpdated: NDLADate, - // format: on + @description("NDLA ID of responsible editor") responsibleId: String, + @description("Date of when the responsible editor was last updated") lastUpdated: NDLADate ) object ConceptResponsible { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptTags.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptTags.scala index 47fde468bc..64ff9da70d 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptTags.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptTags.scala @@ -9,15 +9,13 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Meta image for the concept") +@description("Meta image for the concept") case class ConceptTags( - @(ApiModelProperty @field)(description = "Searchable tags") tags: Seq[String], - @(ApiModelProperty @field)( - description = "The ISO 639-1 language code describing which concept translation these tags belongs to" + @description("Searchable tags") tags: Seq[String], + @description( + "The ISO 639-1 language code describing which concept translation these tags belongs to" ) language: String ) diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptTitle.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptTitle.scala index f93ec715b5..6f41953ae0 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptTitle.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptTitle.scala @@ -9,14 +9,12 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about the concept") +@description("Information about the concept") case class ConceptTitle( - @(ApiModelProperty @field)(description = "The title of this concept") title: String, - @(ApiModelProperty @field)(description = "The language of this concept") language: String + @description("The title of this concept") title: String, + @description("The language of this concept") language: String ) object ConceptTitle { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/DraftConceptSearchParams.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/DraftConceptSearchParams.scala index 40917a8b4d..499eec2219 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/DraftConceptSearchParams.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/DraftConceptSearchParams.scala @@ -9,30 +9,28 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description // format: off -@ApiModel(description = "The search parameters") +@description("The search parameters") case class DraftConceptSearchParams( - @(ApiModelProperty @field)(description = "The search query.") query: Option[String], - @(ApiModelProperty @field)(description = "The ISO 639-1 language code describing language used in query-params.") language: Option[String], - @(ApiModelProperty @field)(description = "The page number of the search hits to display.") page: Option[Int], - @(ApiModelProperty @field)(description = "The number of search hits to display for each page.") pageSize: Option[Int], - @(ApiModelProperty @field)(description = "Return only articles that have one of the provided ids.") idList: List[Long], - @(ApiModelProperty @field)(description = "The sorting used on results. Default is by -relevance.") sort: Option[String], - @(ApiModelProperty @field)(description = "Whether to fallback to existing language if not found in selected language.") fallback: Option[Boolean], - @(ApiModelProperty @field)(description = "A search context retrieved from the response header of a previous search.") scrollId: Option[String], - @(ApiModelProperty @field)(description = "A comma-separated list of subjects that should appear in the search.") subjects: Set[String], - @(ApiModelProperty @field)(description = "A comma-separated list of tags to filter the search by.") tags: Set[String], - @(ApiModelProperty @field)(description = "A comma-separated list of statuses that should appear in the search.") status: Set[String], - @(ApiModelProperty @field)(description = "A comma-separated list of users to filter the search by.") users: Seq[String], - @(ApiModelProperty @field)(description = "Embed resource type that should exist in the concepts.") embedResource: Option[String], - @(ApiModelProperty @field)(description = "Embed id attribute that should exist in the concepts.") embedId: Option[String], - @(ApiModelProperty @field)(description = "A comma-separated list of NDLA IDs to filter the search by.") responsibleIds: List[String], - @(ApiModelProperty @field)(description = "The type of concepts to return.") conceptType: Option[String], - @(ApiModelProperty @field)(description = "A list of index paths to aggregate over") aggregatePaths: List[String], + @description("The search query.") query: Option[String], + @description("The ISO 639-1 language code describing language used in query-params.") language: Option[String], + @description("The page number of the search hits to display.") page: Option[Int], + @description("The number of search hits to display for each page.") pageSize: Option[Int], + @description("Return only articles that have one of the provided ids.") idList: List[Long], + @description("The sorting used on results. Default is by -relevance.") sort: Option[String], + @description("Whether to fallback to existing language if not found in selected language.") fallback: Option[Boolean], + @description("A search context retrieved from the response header of a previous search.") scrollId: Option[String], + @description("A comma-separated list of subjects that should appear in the search.") subjects: Set[String], + @description("A comma-separated list of tags to filter the search by.") tags: Set[String], + @description("A comma-separated list of statuses that should appear in the search.") status: Set[String], + @description("A comma-separated list of users to filter the search by.") users: Seq[String], + @description("Embed resource type that should exist in the concepts.") embedResource: Option[String], + @description("Embed id attribute that should exist in the concepts.") embedId: Option[String], + @description("A comma-separated list of NDLA IDs to filter the search by.") responsibleIds: List[String], + @description("The type of concepts to return.") conceptType: Option[String], + @description("A list of index paths to aggregate over") aggregatePaths: List[String], ) object DraftConceptSearchParams{ diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/EditorNote.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/EditorNote.scala index a3573a6b0b..eee8230c61 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/EditorNote.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/EditorNote.scala @@ -10,16 +10,14 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about the editorial notes") +@description("Information about the editorial notes") case class EditorNote( - @(ApiModelProperty @field)(description = "Editorial note") note: String, - @(ApiModelProperty @field)(description = "User which saved the note") updatedBy: String, - @(ApiModelProperty @field)(description = "Status of concept at saved time") status: Status, - @(ApiModelProperty @field)(description = "Timestamp of when note was saved") timestamp: NDLADate + @description("Editorial note") note: String, + @description("User which saved the note") updatedBy: String, + @description("Status of concept at saved time") status: Status, + @description("Timestamp of when note was saved") timestamp: NDLADate ) object EditorNote { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/Gloss.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/Gloss.scala index 21b2dfee7d..08857d41e2 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/Gloss.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/Gloss.scala @@ -9,16 +9,14 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description // format: off -@ApiModel(description = "Information about the gloss example") +@description("Information about the gloss example") case class GlossExample( - @(ApiModelProperty @field)(description = "Example use of the gloss") example: String, - @(ApiModelProperty @field)(description = "Language of the example") language: String, - @(ApiModelProperty @field)(description = "Alternative writing of the example") transcriptions: Map[String, String], + @description("Example use of the gloss") example: String, + @description("Language of the example") language: String, + @description("Alternative writing of the example") transcriptions: Map[String, String], ) object GlossExample { @@ -26,13 +24,13 @@ object GlossExample { implicit val decoder: Decoder[GlossExample] = deriveDecoder } -@ApiModel(description = "Information about the gloss data") +@description("Information about the gloss data") case class GlossData( - @(ApiModelProperty @field)(description = "The gloss itself") gloss: String, - @(ApiModelProperty @field)(description = "Word class / part of speech, ex. noun, adjective, verb, adverb, ...") wordClass: String, - @(ApiModelProperty @field)(description = "Original language of the gloss") originalLanguage: String, - @(ApiModelProperty @field)(description = "Alternative writing of the gloss") transcriptions: Map[String, String], - @(ApiModelProperty @field)(description = "List of examples of how the gloss can be used") examples: List[List[GlossExample]], + @description("The gloss itself") gloss: String, + @description("Word class / part of speech, ex. noun, adjective, verb, adverb, ...") wordClass: String, + @description("Original language of the gloss") originalLanguage: String, + @description("Alternative writing of the gloss") transcriptions: Map[String, String], + @description("List of examples of how the gloss can be used") examples: List[List[GlossExample]], ) // format: on diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NDLAErrors.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NDLAErrors.scala index dbc1fdb670..239a082d2e 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NDLAErrors.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NDLAErrors.scala @@ -9,28 +9,27 @@ package no.ndla.conceptapi.model.api import no.ndla.common.Clock import no.ndla.common.errors.{AccessDeniedException, ValidationException} - -import java.time.LocalDateTime -import scala.annotation.meta.field import no.ndla.conceptapi.Props import no.ndla.conceptapi.integration.DataSource import no.ndla.network.model.HttpRequestException import no.ndla.network.tapir.{AllErrors, TapirErrorHelpers} import no.ndla.search.{IndexNotFoundException, NdlaSearchException} import org.postgresql.util.PSQLException -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description + +import java.time.LocalDateTime -@ApiModel(description = "Information about an error") +@description("Information about an error") case class Error( - @(ApiModelProperty @field)(description = "Code stating the type of error") code: String, - @(ApiModelProperty @field)(description = "Description of the error") description: String, - @(ApiModelProperty @field)(description = "When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() + @description("Code stating the type of error") code: String, + @description("Description of the error") description: String, + @description("When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() ) trait ErrorHelpers extends TapirErrorHelpers { this: Props with Clock with DataSource => - import ErrorHelpers._ import ConceptErrorHelpers._ + import ErrorHelpers._ override def handleErrors: PartialFunction[Throwable, AllErrors] = { case a: AccessDeniedException => forbiddenMsg(a.getMessage) diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NewConceptMetaImage.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NewConceptMetaImage.scala index 6e7749c483..f263ad8ba8 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NewConceptMetaImage.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NewConceptMetaImage.scala @@ -8,13 +8,11 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.ApiModelProperty - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description case class NewConceptMetaImage( - @(ApiModelProperty @field)(description = "The image-api id of the meta image") id: String, - @(ApiModelProperty @field)(description = "The alt text of the meta image") alt: String + @description("The image-api id of the meta image") id: String, + @description("The alt text of the meta image") alt: String ) object NewConceptMetaImage { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/Status.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/Status.scala index c6214295d1..561a3c7a4b 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/Status.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/Status.scala @@ -9,14 +9,12 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Status information for the concept") +@description("Status information for the concept") case class Status( - @(ApiModelProperty @field)(description = "The current status of the concept") current: String, - @(ApiModelProperty @field)(description = "Previous statuses this concept has been in") other: Seq[String] + @description("The current status of the concept") current: String, + @description("Previous statuses this concept has been in") other: Seq[String] ) object Status { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/TagsSearchResult.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/TagsSearchResult.scala index 69e168287a..670e8556fb 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/TagsSearchResult.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/TagsSearchResult.scala @@ -9,17 +9,15 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about tags-search-results") +@description("Information about tags-search-results") case class TagsSearchResult( - @(ApiModelProperty @field)(description = "The total number of tags matching this query") totalCount: Int, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The chosen search language") language: String, - @(ApiModelProperty @field)(description = "The search results") results: Seq[String] + @description("The total number of tags matching this query") totalCount: Int, + @description("For which page results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The chosen search language") language: String, + @description("The search results") results: Seq[String] ) object TagsSearchResult { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ValidationError.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ValidationError.scala index 15377f7a35..7110ec6725 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ValidationError.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ValidationError.scala @@ -8,17 +8,14 @@ package no.ndla.conceptapi.model.api import no.ndla.common.errors.ValidationMessage -import java.time.LocalDateTime - -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field +import java.time.LocalDateTime -@ApiModel(description = "Information about validation errors") +@description("Information about validation errors") case class ValidationError( - @(ApiModelProperty @field)(description = "Code stating the type of error") code: String, - @(ApiModelProperty @field)(description = "Description of the error") description: String, - @(ApiModelProperty @field)(description = "List of validation messages") messages: Seq[ValidationMessage], - @(ApiModelProperty @field)(description = "When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() + @description("Code stating the type of error") code: String, + @description("Description of the error") description: String, + @description("List of validation messages") messages: Seq[ValidationMessage], + @description("When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() ) diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/VisualElement.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/VisualElement.scala index c807862f84..00d0b6a3de 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/VisualElement.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/VisualElement.scala @@ -9,16 +9,13 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description // format: off -@ApiModel(description = "Description of a visual element") +@description("Description of a visual element") case class VisualElement( - @(ApiModelProperty @field)(description = "Html containing the visual element. May contain any legal html element, including the embed-tag") visualElement: String, - @(ApiModelProperty @field)(description = "The ISO 639-1 language code describing which article translation this visual element belongs to") language: String + @description("Html containing the visual element. May contain any legal html element, including the embed-tag") visualElement: String, + @description("The ISO 639-1 language code describing which article translation this visual element belongs to") language: String ) // format: on diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/TestEnvironment.scala b/concept-api/src/test/scala/no/ndla/conceptapi/TestEnvironment.scala index 4f15d43cbb..0d7cce0935 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/TestEnvironment.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/TestEnvironment.scala @@ -24,7 +24,6 @@ import no.ndla.conceptapi.service._ import no.ndla.conceptapi.service.search._ import no.ndla.conceptapi.validation.ContentValidator import no.ndla.network.NdlaClient -import no.ndla.network.scalatra.{NdlaControllerBase, NdlaSwaggerSupport} import no.ndla.network.tapir.{NdlaMiddleware, Routes, Service} import no.ndla.search.{BaseIndexService, Elastic4sClient} import org.mockito.scalatest.MockitoSugar @@ -34,8 +33,6 @@ trait TestEnvironment with PublishedConceptRepository with DraftConceptController with ConceptControllerHelpers - with NdlaControllerBase - with NdlaSwaggerSupport with PublishedConceptController with SearchConverterService with PublishedConceptSearchService diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/Article.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/Article.scala index 73ad9b37f2..e55f036150 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/Article.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/Article.scala @@ -7,51 +7,49 @@ package no.ndla.draftapi.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import no.ndla.common.implicits._ import no.ndla.common.model.NDLADate -import no.ndla.common.model.api.{DraftCopyright, RelatedContent, RelatedContentLink} import no.ndla.common.model.api.draft.Comment -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field -import no.ndla.common.implicits._ +import no.ndla.common.model.api.{DraftCopyright, RelatedContent, RelatedContentLink} +import sttp.tapir.Schema.annotations.description // format: off -@ApiModel(description = "Information about the article") +@description("Information about the article") case class Article( - @(ApiModelProperty @field)(description = "The unique id of the article") id: Long, - @(ApiModelProperty @field)(description = "Link to article on old platform") oldNdlaUrl: Option[String], - @(ApiModelProperty @field)(description = "The revision number for the article") revision: Int, - @(ApiModelProperty @field)(description = "The status of this article", allowableValues = "CREATED,IMPORTED,DRAFT,SKETCH,USER_TEST,QUALITY_ASSURED,AWAITING_QUALITY_ASSURANCE") status: Status, - @(ApiModelProperty @field)(description = "Available titles for the article") title: Option[ArticleTitle], - @(ApiModelProperty @field)(description = "The content of the article in available languages") content: Option[ArticleContent], - @(ApiModelProperty @field)(description = "Describes the copyright information for the article") copyright: Option[DraftCopyright], - @(ApiModelProperty @field)(description = "Searchable tags for the article") tags: Option[ArticleTag], - @(ApiModelProperty @field)(description = "Required libraries in order to render the article") requiredLibraries: Seq[RequiredLibrary], - @(ApiModelProperty @field)(description = "A visual element article") visualElement: Option[VisualElement], - @(ApiModelProperty @field)(description = "An introduction for the article") introduction: Option[ArticleIntroduction], - @(ApiModelProperty @field)(description = "Meta description for the article") metaDescription: Option[ArticleMetaDescription], - @(ApiModelProperty @field)(description = "Meta image for the article") metaImage: Option[ArticleMetaImage], - @(ApiModelProperty @field)(description = "When the article was created") created: NDLADate, - @(ApiModelProperty @field)(description = "When the article was last updated") updated: NDLADate, - @(ApiModelProperty @field)(description = "By whom the article was last updated") updatedBy: String, - @(ApiModelProperty @field)(description = "When the article was last published") published: NDLADate, - @(ApiModelProperty @field)(description = "The type of article this is. Possible values are frontpage-article, standard, topic-article") articleType: String, - @(ApiModelProperty @field)(description = "The languages this article supports") supportedLanguages: Seq[String], - @(ApiModelProperty @field)(description = "The notes for this article draft") notes: Seq[EditorNote], - @(ApiModelProperty @field)(description = "The labels attached to this article; meant for editors.") editorLabels: Seq[String], - @(ApiModelProperty @field)(description = "A list of codes from GREP API connected to the article") grepCodes: Seq[String], - @(ApiModelProperty @field)(description = "A list of conceptIds connected to the article") conceptIds: Seq[Long], - @(ApiModelProperty @field)(description = "Value that dictates who gets to see the article. Possible values are: everyone/teacher") availability: String, - @(ApiModelProperty @field)(description = "A list of content related to the article") relatedContent: Seq[RelatedContent], - @(ApiModelProperty @field)(description = "A list of revisions planned for the article") revisions: Seq[RevisionMeta], - @(ApiModelProperty @field)(description = "Object with data representing the editor responsible for this article") responsible: Option[DraftResponsible], - @(ApiModelProperty @field)(description = "The path to the frontpage article") slug: Option[String], - @(ApiModelProperty @field)(description = "Information about comments attached to the article") comments: Seq[Comment], - @(ApiModelProperty @field)(description = "If the article should be prioritized") prioritized: Boolean, - @(ApiModelProperty @field)(description = "If the article should be prioritized. Possible values are prioritized, on-hold, unspecified") priority: String, - @(ApiModelProperty @field)(description = "If the article has been edited after last status or responsible change") started: Boolean + @description("The unique id of the article") id: Long, + @description("Link to article on old platform") oldNdlaUrl: Option[String], + @description("The revision number for the article") revision: Int, + @description("The status of this article") status: Status, + @description("Available titles for the article") title: Option[ArticleTitle], + @description("The content of the article in available languages") content: Option[ArticleContent], + @description("Describes the copyright information for the article") copyright: Option[DraftCopyright], + @description("Searchable tags for the article") tags: Option[ArticleTag], + @description("Required libraries in order to render the article") requiredLibraries: Seq[RequiredLibrary], + @description("A visual element article") visualElement: Option[VisualElement], + @description("An introduction for the article") introduction: Option[ArticleIntroduction], + @description("Meta description for the article") metaDescription: Option[ArticleMetaDescription], + @description("Meta image for the article") metaImage: Option[ArticleMetaImage], + @description("When the article was created") created: NDLADate, + @description("When the article was last updated") updated: NDLADate, + @description("By whom the article was last updated") updatedBy: String, + @description("When the article was last published") published: NDLADate, + @description("The type of article this is. Possible values are frontpage-article, standard, topic-article") articleType: String, + @description("The languages this article supports") supportedLanguages: Seq[String], + @description("The notes for this article draft") notes: Seq[EditorNote], + @description("The labels attached to this article; meant for editors.") editorLabels: Seq[String], + @description("A list of codes from GREP API connected to the article") grepCodes: Seq[String], + @description("A list of conceptIds connected to the article") conceptIds: Seq[Long], + @description("Value that dictates who gets to see the article. Possible values are: everyone/teacher") availability: String, + @description("A list of content related to the article") relatedContent: Seq[RelatedContent], + @description("A list of revisions planned for the article") revisions: Seq[RevisionMeta], + @description("Object with data representing the editor responsible for this article") responsible: Option[DraftResponsible], + @description("The path to the frontpage article") slug: Option[String], + @description("Information about comments attached to the article") comments: Seq[Comment], + @description("If the article should be prioritized") prioritized: Boolean, + @description("If the article should be prioritized. Possible values are prioritized, on-hold, unspecified") priority: String, + @description("If the article has been edited after last status or responsible change") started: Boolean ) object Article { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleContent.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleContent.scala index b885c2d941..d3c763a5a9 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleContent.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleContent.scala @@ -9,16 +9,12 @@ package no.ndla.draftapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "The content of the article in the specified language") +@description("The content of the article in the specified language") case class ArticleContent( - @(ApiModelProperty @field)(description = "The html content") content: String, - @(ApiModelProperty @field)( - description = "ISO 639-1 code that represents the language used in the content" - ) language: String + @description("The html content") content: String, + @description("ISO 639-1 code that represents the language used in the content") language: String ) object ArticleContent { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleDomainDump.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleDomainDump.scala index b3d1cefa23..03280a2d94 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleDomainDump.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleDomainDump.scala @@ -8,14 +8,12 @@ package no.ndla.draftapi.model.api import no.ndla.common.model.domain.draft -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about articles") +@description("Information about articles") case class ArticleDomainDump( - @(ApiModelProperty @field)(description = "The total number of articles in the database") totalCount: Long, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The search results") results: Seq[draft.Draft] + @description("The total number of articles in the database") totalCount: Long, + @description("For which page results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The search results") results: Seq[draft.Draft] ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleIntroduction.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleIntroduction.scala index 7c37af312b..df74aaa7a9 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleIntroduction.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleIntroduction.scala @@ -7,18 +7,16 @@ package no.ndla.draftapi.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Description of the article introduction") +@description("Description of the article introduction") case class ArticleIntroduction( - @(ApiModelProperty @field)(description = "The introduction content") introduction: String, - @(ApiModelProperty @field)(description = "The html introduction content") htmlIntroduction: String, - @(ApiModelProperty @field)( - description = "The ISO 639-1 language code describing which article translation this introduction belongs to" + @description("The introduction content") introduction: String, + @description("The html introduction content") htmlIntroduction: String, + @description( + "The ISO 639-1 language code describing which article translation this introduction belongs to" ) language: String ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleMetaDescription.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleMetaDescription.scala index 66ddf031c8..ffd9b187a1 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleMetaDescription.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleMetaDescription.scala @@ -7,17 +7,15 @@ package no.ndla.draftapi.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Meta description of the article") +@description("Meta description of the article") case class ArticleMetaDescription( - @(ApiModelProperty @field)(description = "The meta description") metaDescription: String, - @(ApiModelProperty @field)( - description = "The ISO 639-1 language code describing which article translation this meta description belongs to" + @description("The meta description") metaDescription: String, + @description( + "The ISO 639-1 language code describing which article translation this meta description belongs to" ) language: String ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleMetaImage.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleMetaImage.scala index 06084b242b..5c35b057e5 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleMetaImage.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleMetaImage.scala @@ -9,16 +9,14 @@ package no.ndla.draftapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Meta description of the article") +@description("Meta description of the article") case class ArticleMetaImage( - @(ApiModelProperty @field)(description = "The meta image") url: String, - @(ApiModelProperty @field)(description = "The meta image alt text") alt: String, - @(ApiModelProperty @field)( - description = "The ISO 639-1 language code describing which article translation this meta image belongs to" + @description("The meta image") url: String, + @description("The meta image alt text") alt: String, + @description( + "The ISO 639-1 language code describing which article translation this meta image belongs to" ) language: String ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticlePublishReport.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticlePublishReport.scala index 069f4ed975..a72330af68 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticlePublishReport.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticlePublishReport.scala @@ -7,24 +7,18 @@ package no.ndla.draftapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Id for a single Article") +@description("Id for a single Article") case class ArticlePublishReport( - @(ApiModelProperty @field)(description = "The ids of articles which was successfully (un)published") succeeded: Seq[ - Long - ], - @(ApiModelProperty @field)(description = "The ids of articles which failed to (un)publish") failed: Seq[ - FailedArticlePublish - ] + @description("The ids of articles which was successfully (un)published") succeeded: Seq[Long], + @description("The ids of articles which failed to (un)publish") failed: Seq[FailedArticlePublish] ) { def addFailed(fail: FailedArticlePublish): ArticlePublishReport = this.copy(failed = failed :+ fail) def addSuccessful(id: Long): ArticlePublishReport = this.copy(succeeded = succeeded :+ id) } case class FailedArticlePublish( - @(ApiModelProperty @field)(description = "The id of an article which failed to be (un)published") id: Long, - @(ApiModelProperty @field)(description = "A message describing the cause") message: String + @description("The id of an article which failed to be (un)published") id: Long, + @description("A message describing the cause") message: String ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSearchParams.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSearchParams.scala index 5838f66c4b..cfda0f57ad 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSearchParams.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSearchParams.scala @@ -9,24 +9,22 @@ package no.ndla.draftapi.model.api import io.circe.generic.extras.semiauto.{deriveConfiguredDecoder, deriveConfiguredEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description // format: off -@ApiModel(description = "The search parameters") +@description("The search parameters") case class ArticleSearchParams( - @(ApiModelProperty @field)(description = "The search query") query: Option[String], - @(ApiModelProperty @field)(description = "The ISO 639-1 language code describing language used in query-params") language: Option[String], - @(ApiModelProperty @field)(description = "Return only articles with provided license.") license: Option[String], - @(ApiModelProperty @field)(description = "The page number of the search hits to display.") page: Option[Int], - @(ApiModelProperty @field)(description = "The number of search hits to display for each page.") pageSize: Option[Int], - @(ApiModelProperty @field)(description = "Return only articles that have one of the provided ids") idList: List[Long] = List.empty, - @(ApiModelProperty @field)(description = "Return only articles of specific type(s)") articleTypes: List[String] = List.empty, - @(ApiModelProperty @field)(description = "The sorting used on results. Default is by -relevance.") sort: Option[String], - @(ApiModelProperty @field)(description = "A search context retrieved from the response header of a previous search.") scrollId: Option[String], - @(ApiModelProperty @field)(description = "Fallback to some existing language if language is specified.") fallback: Option[Boolean], - @(ApiModelProperty @field)(description = "Return only articles containing codes from GREP API") grepCodes: List[String] = List.empty + @description("The search query") query: Option[String], + @description("The ISO 639-1 language code describing language used in query-params") language: Option[String], + @description("Return only articles with provided license.") license: Option[String], + @description("The page number of the search hits to display.") page: Option[Int], + @description("The number of search hits to display for each page.") pageSize: Option[Int], + @description("Return only articles that have one of the provided ids") idList: List[Long] = List.empty, + @description("Return only articles of specific type(s)") articleTypes: List[String] = List.empty, + @description("The sorting used on results. Default is by -relevance.") sort: Option[String], + @description("A search context retrieved from the response header of a previous search.") scrollId: Option[String], + @description("Fallback to some existing language if language is specified.") fallback: Option[Boolean], + @description("Return only articles containing codes from GREP API") grepCodes: List[String] = List.empty ) object ArticleSearchParams { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSearchResult.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSearchResult.scala index cc1c2e865f..15e2b3fddc 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSearchResult.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSearchResult.scala @@ -7,13 +7,12 @@ package no.ndla.draftapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Information about search-results") +@description("Information about search-results") case class ArticleSearchResult( - @(ApiModelProperty @field)(description = "The total number of articles matching this query") totalCount: Long, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Option[Int], - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The search results") results: Seq[ArticleSummary] + @description("The total number of articles matching this query") totalCount: Long, + @description("For which page results are shown from") page: Option[Int], + @description("The number of results per page") pageSize: Int, + @description("The search results") results: Seq[ArticleSummary] ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSummary.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSummary.scala index f14dff64a8..b10ba3d122 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSummary.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleSummary.scala @@ -8,26 +8,23 @@ package no.ndla.draftapi.model.api import no.ndla.common.model.NDLADate -import org.scalatra.swagger.annotations.ApiModel -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description // format: off -@ApiModel(description = "Short summary of information about the article") +@description("Short summary of information about the article") case class ArticleSummary( - @(ApiModelProperty @field)(description = "The unique id of the article") id: Long, - @(ApiModelProperty @field)(description = "The title of the article") title: ArticleTitle, - @(ApiModelProperty @field)(description = "A visual element article") visualElement: Option[VisualElement], - @(ApiModelProperty @field)(description = "An introduction for the article") introduction: Option[ArticleIntroduction], - @(ApiModelProperty @field)(description = "The full url to where the complete information about the article can be found") url: String, - @(ApiModelProperty @field)(description = "Describes the license of the article") license: String, - @(ApiModelProperty @field)(description = "The type of article this is. Possible values are frontpage-article, standard, topic-article") articleType: String, - @(ApiModelProperty @field)(description = "A list of available languages for this audio") supportedLanguages: Seq[String], - @(ApiModelProperty @field)(description = "Searchable tags for the article") tags: Option[ArticleTag], - @(ApiModelProperty @field)(description = "The notes for this draft article") notes: Seq[String], - @(ApiModelProperty @field)(description = "The users saved for this draft article") users: Seq[String], - @(ApiModelProperty @field)(description = "The codes from GREP API registered for this draft article") grepCodes: Seq[String], - @(ApiModelProperty @field)(description = "The status of this article", allowableValues = "CREATED,IMPORTED,DRAFT,SKETCH,USER_TEST,QUALITY_ASSURED,AWAITING_QUALITY_ASSURANCE") status: Status, - @(ApiModelProperty @field)(description = "When the article was last updated") updated: NDLADate + @description("The unique id of the article") id: Long, + @description("The title of the article") title: ArticleTitle, + @description("A visual element article") visualElement: Option[VisualElement], + @description("An introduction for the article") introduction: Option[ArticleIntroduction], + @description("The full url to where the complete information about the article can be found") url: String, + @description("Describes the license of the article") license: String, + @description("The type of article this is. Possible values are frontpage-article, standard, topic-article") articleType: String, + @description("A list of available languages for this audio") supportedLanguages: Seq[String], + @description("Searchable tags for the article") tags: Option[ArticleTag], + @description("The notes for this draft article") notes: Seq[String], + @description("The users saved for this draft article") users: Seq[String], + @description("The codes from GREP API registered for this draft article") grepCodes: Seq[String], + @description("The status of this article") status: Status, + @description("When the article was last updated") updated: NDLADate ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleTag.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleTag.scala index 51d4117621..8430cb7aae 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleTag.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleTag.scala @@ -9,14 +9,12 @@ package no.ndla.draftapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Description of the tags of the article") +@description("Description of the tags of the article") case class ArticleTag( - @(ApiModelProperty @field)(description = "The searchable tag.") tags: Seq[String], - @(ApiModelProperty @field)(description = "ISO 639-1 code that represents the language used in tag") language: String + @description("The searchable tag.") tags: Seq[String], + @description("ISO 639-1 code that represents the language used in tag") language: String ) object ArticleTag { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleTitle.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleTitle.scala index c229b84734..aff8b18d08 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleTitle.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ArticleTitle.scala @@ -7,19 +7,15 @@ package no.ndla.draftapi.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Description of a title") +@description("Description of a title") case class ArticleTitle( - @(ApiModelProperty @field)(description = "The freetext title of the article") title: String, - @(ApiModelProperty @field)(description = "The freetext html title of the article") htmlTitle: String, - @(ApiModelProperty @field)( - description = "ISO 639-1 code that represents the language used in title" - ) language: String + @description("The freetext title of the article") title: String, + @description("The freetext html title of the article") htmlTitle: String, + @description("ISO 639-1 code that represents the language used in title") language: String ) object ArticleTitle { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ContentId.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ContentId.scala index b6d4e0c4be..9a9900c52d 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ContentId.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ContentId.scala @@ -7,9 +7,7 @@ package no.ndla.draftapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Id for a single Article") -case class ContentId(@(ApiModelProperty @field)(description = "The unique id of the article") id: Long) +@description("Id for a single Article") +case class ContentId(@description("The unique id of the article") id: Long) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/DraftResponsible.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/DraftResponsible.scala index 0e1b356f8e..02a38cbf04 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/DraftResponsible.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/DraftResponsible.scala @@ -10,15 +10,13 @@ package no.ndla.draftapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description // format: off -@ApiModel(description = "Information about the responsible") +@description("Information about the responsible") case class DraftResponsible( - @(ApiModelProperty @field)(description = "NDLA ID of responsible editor") responsibleId: String, - @(ApiModelProperty @field)(description = "Date of when the responsible editor was last updated") lastUpdated: NDLADate, + @description("NDLA ID of responsible editor") responsibleId: String, + @description("Date of when the responsible editor was last updated") lastUpdated: NDLADate, ) object DraftResponsible { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/EditorNote.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/EditorNote.scala index c4297ea193..c3c07dc5f7 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/EditorNote.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/EditorNote.scala @@ -9,16 +9,14 @@ package no.ndla.draftapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about the editorial notes") +@description("Information about the editorial notes") case class EditorNote( - @(ApiModelProperty @field)(description = "Editorial note") note: String, - @(ApiModelProperty @field)(description = "User which saved the note") user: String, - @(ApiModelProperty @field)(description = "Status of article at saved time") status: Status, - @(ApiModelProperty @field)(description = "Timestamp of when note was saved") timestamp: NDLADate + @description("Editorial note") note: String, + @description("User which saved the note") user: String, + @description("Status of article at saved time") status: Status, + @description("Timestamp of when note was saved") timestamp: NDLADate ) object EditorNote { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/MultiPartialPublishResult.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/MultiPartialPublishResult.scala index cde3fadd6a..884e53ee9b 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/MultiPartialPublishResult.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/MultiPartialPublishResult.scala @@ -7,19 +7,16 @@ package no.ndla.draftapi.model.api -import org.scalatra.swagger.annotations.ApiModel -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Single failed result") +@description("Single failed result") case class PartialPublishFailure( - @(ApiModelProperty @field)(description = "Id of the article in question") id: Long, - @(ApiModelProperty @field)(description = "Error message") message: String + @description("Id of the article in question") id: Long, + @description("Error message") message: String ) -@ApiModel(description = "A list of articles that were partial published to article-api") +@description("A list of articles that were partial published to article-api") case class MultiPartialPublishResult( - @(ApiModelProperty @field)(description = "Successful ids") successes: Seq[Long], - @(ApiModelProperty @field)(description = "Failed ids with error messages") failures: Seq[PartialPublishFailure] + @description("Successful ids") successes: Seq[Long], + @description("Failed ids with error messages") failures: Seq[PartialPublishFailure] ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/NDLAErrors.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/NDLAErrors.scala index bc577245e1..9f8ca677a6 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/NDLAErrors.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/NDLAErrors.scala @@ -9,22 +9,21 @@ package no.ndla.draftapi.model.api import no.ndla.common.Clock import no.ndla.common.errors.{AccessDeniedException, FileTooBigException, ValidationException} - -import java.time.LocalDateTime -import scala.annotation.meta.field import no.ndla.draftapi.Props import no.ndla.draftapi.integration.DataSource import no.ndla.network.model.HttpRequestException import no.ndla.network.tapir.{AllErrors, ErrorBody, TapirErrorHelpers} import no.ndla.search.{IndexNotFoundException, NdlaSearchException} import org.postgresql.util.PSQLException -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description + +import java.time.LocalDateTime -@ApiModel(description = "Information about an error") +@description("Information about an error") case class Error( - @(ApiModelProperty @field)(description = "Code stating the type of error") code: String, - @(ApiModelProperty @field)(description = "Description of the error") description: String, - @(ApiModelProperty @field)(description = "When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() + @description("Code stating the type of error") code: String, + @description("Description of the error") description: String, + @description("When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() ) trait ErrorHelpers extends TapirErrorHelpers { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/NewArticleMetaImage.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/NewArticleMetaImage.scala index 670d12368e..34aed6df0a 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/NewArticleMetaImage.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/NewArticleMetaImage.scala @@ -7,16 +7,14 @@ package no.ndla.draftapi.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import io.circe.{Decoder, Encoder} import sttp.tapir.Schema - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description case class NewArticleMetaImage( - @(ApiModelProperty @field)(description = "The image-api id of the meta image") id: String, - @(ApiModelProperty @field)(description = "The alt text of the meta image") alt: String + @description("The image-api id of the meta image") id: String, + @description("The alt text of the meta image") alt: String ) object NewArticleMetaImage { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/NewComment.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/NewComment.scala index 295db153b2..eab9289d1e 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/NewComment.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/NewComment.scala @@ -7,16 +7,14 @@ package no.ndla.draftapi.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Information about a comment attached to an article") +@description("Information about a comment attached to an article") case class NewComment( - @(ApiModelProperty @field)(description = "Content of the comment") content: String, - @(ApiModelProperty @field)(description = "If the comment is open or closed") isOpen: Option[Boolean] + @description("Content of the comment") content: String, + @description("If the comment is open or closed") isOpen: Option[Boolean] ) object NewComment { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/PartialPublishArticle.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/PartialPublishArticle.scala index e9d1a66547..42f534c1d8 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/PartialPublishArticle.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/PartialPublishArticle.scala @@ -8,8 +8,7 @@ package no.ndla.draftapi.model.api import enumeratum._ -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description sealed trait PartialArticleFields extends EnumEntry @@ -26,8 +25,8 @@ object PartialArticleFields extends Enum[PartialArticleFields] with CirceEnum[Pa } // format: off -@ApiModel(description = "Partial data about articles to publish in bulk") +@description("Partial data about articles to publish in bulk") case class PartialBulkArticles( - @(ApiModelProperty @field)(description = "A list of article ids to partially publish") articleIds: Seq[Long], - @(ApiModelProperty @field)(description = "A list of fields that should be partially published") fields: Seq[PartialArticleFields], + @description("A list of article ids to partially publish") articleIds: Seq[Long], + @description("A list of fields that should be partially published") fields: Seq[PartialArticleFields], ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/RequiredLibrary.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/RequiredLibrary.scala index 90f0766aec..89ee47031d 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/RequiredLibrary.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/RequiredLibrary.scala @@ -9,15 +9,13 @@ package no.ndla.draftapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about a library required to render the article") +@description("Information about a library required to render the article") case class RequiredLibrary( - @(ApiModelProperty @field)(description = "The type of the library. E.g. CSS or JavaScript") mediaType: String, - @(ApiModelProperty @field)(description = "The name of the library") name: String, - @(ApiModelProperty @field)(description = "The full url to where the library can be downloaded") url: String + @description("The type of the library. E.g. CSS or JavaScript") mediaType: String, + @description("The name of the library") name: String, + @description("The full url to where the library can be downloaded") url: String ) object RequiredLibrary { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/RevisionMeta.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/RevisionMeta.scala index 8b2c8ea3ea..119cb10494 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/RevisionMeta.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/RevisionMeta.scala @@ -10,15 +10,15 @@ package no.ndla.draftapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description // format: off -@ApiModel(description = "Information about the editorial notes") +@description("Information about the editorial notes") case class RevisionMeta( - @ApiModelProperty(description = "An unique uuid of the revision. If none supplied, one is generated.") id: Option[String], - @ApiModelProperty(description = "A date on which the article would need to be revised") revisionDate: NDLADate, - @ApiModelProperty(description = "Notes to keep track of what needs to happen before revision") note: String, - @ApiModelProperty(description = "Status of a revision, either 'revised' or 'needs-revision'", allowableValues = "revised,needs-revision") status: String + @description("An unique uuid of the revision. If none supplied, one is generated.") id: Option[String], + @description("A date on which the article would need to be revised") revisionDate: NDLADate, + @description("Notes to keep track of what needs to happen before revision") note: String, + @description("Status of a revision, either 'revised' or 'needs-revision'") status: String ) object RevisionMeta { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/SearchResult.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/SearchResult.scala index 68a774448b..752a7ac8d0 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/SearchResult.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/SearchResult.scala @@ -7,47 +7,43 @@ package no.ndla.draftapi.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Information about search-results") +@description("Information about search-results") case class SearchResult( - @(ApiModelProperty @field)(description = "The total number of articles matching this query") totalCount: Long, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The chosen search language") language: String, - @(ApiModelProperty @field)(description = "The search results") results: Seq[ArticleSummary] + @description("The total number of articles matching this query") totalCount: Long, + @description("For which page results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The chosen search language") language: String, + @description("The search results") results: Seq[ArticleSummary] ) -@ApiModel(description = "Information and metadata about codes from GREP API") +@description("Information and metadata about codes from GREP API") case class GrepCodesSearchResult( - @(ApiModelProperty @field)( - description = "The total number of codes from GREP API matching this query" - ) totalCount: Long, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The search results") results: Seq[String] + @description("The total number of codes from GREP API matching this query") totalCount: Long, + @description("For which page results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The search results") results: Seq[String] ) -@ApiModel(description = "Information about tags-search-results") +@description("Information about tags-search-results") case class TagsSearchResult( - @(ApiModelProperty @field)(description = "The total number of tags matching this query") totalCount: Long, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The chosen search language") language: String, - @(ApiModelProperty @field)(description = "The search results") results: Seq[String] + @description("The total number of tags matching this query") totalCount: Long, + @description("For which page results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The chosen search language") language: String, + @description("The search results") results: Seq[String] ) -@ApiModel(description = "Information about articles") +@description("Information about articles") case class ArticleDump( - @(ApiModelProperty @field)(description = "The total number of articles in the database") totalCount: Long, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The chosen search language") language: String, - @(ApiModelProperty @field)(description = "The search results") results: Seq[Article] + @description("The total number of articles in the database") totalCount: Long, + @description("For which page results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The chosen search language") language: String, + @description("The search results") results: Seq[Article] ) object ArticleDump { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/Status.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/Status.scala index ca819803b5..d3f8219efa 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/Status.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/Status.scala @@ -7,15 +7,13 @@ package no.ndla.draftapi.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import org.scalatra.swagger.annotations.ApiModelProperty - -import scala.annotation.meta.field +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description case class Status( - @(ApiModelProperty @field)(description = "The current status of the article") current: String, - @(ApiModelProperty @field)(description = "Previous statuses this article has been in") other: Seq[String] + @description("The current status of the article") current: String, + @description("Previous statuses this article has been in") other: Seq[String] ) object Status { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/UpdatedComment.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/UpdatedComment.scala index 829b63abcf..c284a8dcf0 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/UpdatedComment.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/UpdatedComment.scala @@ -7,18 +7,16 @@ package no.ndla.draftapi.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Information about a comment attached to an article") +@description("Information about a comment attached to an article") case class UpdatedComment( - @(ApiModelProperty @field)(description = "Id of the comment") id: Option[String], - @(ApiModelProperty @field)(description = "Content of the comment") content: String, - @(ApiModelProperty @field)(description = "If the comment is open or closed") isOpen: Option[Boolean], - @(ApiModelProperty @field)(description = "If the comment is solved or not") solved: Option[Boolean] + @description("Id of the comment") id: Option[String], + @description("Content of the comment") content: String, + @description("If the comment is open or closed") isOpen: Option[Boolean], + @description("If the comment is solved or not") solved: Option[Boolean] ) object UpdatedComment { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/UpdatedUserData.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/UpdatedUserData.scala index 03bf22336a..8cbe1c94d0 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/UpdatedUserData.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/UpdatedUserData.scala @@ -7,14 +7,12 @@ package no.ndla.draftapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about user data") +@description("Information about user data") case class UpdatedUserData( - @(ApiModelProperty @field)(description = "User's saved searches") savedSearches: Option[Seq[String]], - @(ApiModelProperty @field)(description = "User's last edited articles") latestEditedArticles: Option[Seq[String]], - @(ApiModelProperty @field)(description = "User's last edited concepts") latestEditedConcepts: Option[Seq[String]], - @(ApiModelProperty @field)(description = "User's favorite subjects") favoriteSubjects: Option[Seq[String]] + @description("User's saved searches") savedSearches: Option[Seq[String]], + @description("User's last edited articles") latestEditedArticles: Option[Seq[String]], + @description("User's last edited concepts") latestEditedConcepts: Option[Seq[String]], + @description("User's favorite subjects") favoriteSubjects: Option[Seq[String]] ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/UploadedFile.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/UploadedFile.scala index 1628086240..fe99c471d6 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/UploadedFile.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/UploadedFile.scala @@ -6,14 +6,12 @@ */ package no.ndla.draftapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about the uploaded file") +@description("Information about the uploaded file") case class UploadedFile( - @(ApiModelProperty @field)(description = "Uploaded file's basename") filename: String, - @(ApiModelProperty @field)(description = "Uploaded file's mime type") mime: String, - @(ApiModelProperty @field)(description = "Uploaded file's file extension") extension: String, - @(ApiModelProperty @field)(description = "Full path of uploaded file") path: String + @description("Uploaded file's basename") filename: String, + @description("Uploaded file's mime type") mime: String, + @description("Uploaded file's file extension") extension: String, + @description("Full path of uploaded file") path: String ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/UserData.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/UserData.scala index ab0225d5e4..534e2337cc 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/UserData.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/UserData.scala @@ -7,13 +7,13 @@ package no.ndla.draftapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Information about user data") +@description("Information about user data") case class UserData( - @ApiModelProperty(description = "The auth0 id of the user") userId: String, - @ApiModelProperty(description = "User's saved searches") savedSearches: Option[Seq[String]], - @ApiModelProperty(description = "User's last edited articles") latestEditedArticles: Option[Seq[String]], - @ApiModelProperty(description = "User's last edited concepts") latestEditedConcepts: Option[Seq[String]], - @ApiModelProperty(description = "User's favorite subjects") favoriteSubjects: Option[Seq[String]] + @description("The auth0 id of the user") userId: String, + @description("User's saved searches") savedSearches: Option[Seq[String]], + @description("User's last edited articles") latestEditedArticles: Option[Seq[String]], + @description("User's last edited concepts") latestEditedConcepts: Option[Seq[String]], + @description("User's favorite subjects") favoriteSubjects: Option[Seq[String]] ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ValidationError.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ValidationError.scala index cf67d2ef61..f0e0c8c017 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/ValidationError.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/ValidationError.scala @@ -8,17 +8,14 @@ package no.ndla.draftapi.model.api import no.ndla.common.errors.ValidationMessage -import java.time.LocalDateTime - -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field +import java.time.LocalDateTime -@ApiModel(description = "Information about validation errors") +@description("Information about validation errors") case class ValidationError( - @(ApiModelProperty @field)(description = "Code stating the type of error") code: String, - @(ApiModelProperty @field)(description = "Description of the error") description: String, - @(ApiModelProperty @field)(description = "List of validation messages") messages: Seq[ValidationMessage], - @(ApiModelProperty @field)(description = "When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() + @description("Code stating the type of error") code: String, + @description("Description of the error") description: String, + @description("List of validation messages") messages: Seq[ValidationMessage], + @description("When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() ) diff --git a/draft-api/src/main/scala/no/ndla/draftapi/model/api/VisualElement.scala b/draft-api/src/main/scala/no/ndla/draftapi/model/api/VisualElement.scala index 18da00cb08..fbcf533c24 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/model/api/VisualElement.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/model/api/VisualElement.scala @@ -9,18 +9,15 @@ package no.ndla.draftapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Description of a visual element") +@description("Description of a visual element") case class VisualElement( - @(ApiModelProperty @field)( - description = "Html containing the visual element. May contain any legal html element, including the embed-tag" + @description( + "Html containing the visual element. May contain any legal html element, including the embed-tag" ) visualElement: String, - @(ApiModelProperty @field)( - description = "The ISO 639-1 language code describing which article translation this visual element belongs to" + @description( + "The ISO 639-1 language code describing which article translation this visual element belongs to" ) language: String ) diff --git a/image-api/src/main/scala/no/ndla/imageapi/ComponentRegistry.scala b/image-api/src/main/scala/no/ndla/imageapi/ComponentRegistry.scala index 7a6be8b87f..2b5dd07a3c 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/ComponentRegistry.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/ComponentRegistry.scala @@ -26,7 +26,6 @@ import no.ndla.imageapi.service.search.{ TagSearchService } import no.ndla.network.NdlaClient -import no.ndla.network.scalatra.{NdlaControllerBase, NdlaSwaggerSupport} import no.ndla.network.tapir.{ NdlaMiddleware, Routes, @@ -68,8 +67,6 @@ class ComponentRegistry(properties: ImageApiProperties) with Props with DBMigrator with ErrorHelpers - with NdlaControllerBase - with NdlaSwaggerSupport with Random with Routes[Eff] with NdlaMiddleware diff --git a/image-api/src/main/scala/no/ndla/imageapi/controller/BaseImageController.scala b/image-api/src/main/scala/no/ndla/imageapi/controller/BaseImageController.scala index 026dabedfc..9494eb0179 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/controller/BaseImageController.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/controller/BaseImageController.scala @@ -13,7 +13,6 @@ import no.ndla.common.model.domain.UploadedFile import no.ndla.imageapi.Props import no.ndla.imageapi.model.domain.{ModelReleasedStatus, Sort} import no.ndla.language.Language -import no.ndla.network.scalatra.NdlaSwaggerSupport import sttp.model.Part import sttp.tapir._ import sttp.tapir.model.{CommaSeparated, Delimited} @@ -22,7 +21,7 @@ import java.io.File import scala.util.{Failure, Try} trait BaseImageController { - this: Props with NdlaSwaggerSupport => + this: Props => /** Base class for sharing code between Image controllers. */ trait BaseImageController { diff --git a/image-api/src/main/scala/no/ndla/imageapi/controller/RawController.scala b/image-api/src/main/scala/no/ndla/imageapi/controller/RawController.scala index 61cfaf1ede..205e4bb6e0 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/controller/RawController.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/controller/RawController.scala @@ -14,7 +14,6 @@ import no.ndla.imageapi.model.api.ErrorHelpers import no.ndla.imageapi.model.domain.ImageStream import no.ndla.imageapi.repository.ImageRepository import no.ndla.imageapi.service.{ImageConverter, ImageStorageService, ReadService} -import no.ndla.network.scalatra.NdlaSwaggerSupport import no.ndla.network.tapir.{AllErrors, DynamicHeaders, Service} import no.ndla.network.tapir.TapirErrors.errorOutputsFor import sttp.tapir._ @@ -24,13 +23,7 @@ import java.io.InputStream import scala.util.{Failure, Success, Try} trait RawController { - this: ImageStorageService - with ImageConverter - with ImageRepository - with ErrorHelpers - with Props - with ReadService - with NdlaSwaggerSupport => + this: ImageStorageService with ImageConverter with ImageRepository with ErrorHelpers with Props with ReadService => val rawController: RawController class RawController extends Service[Eff] { diff --git a/image-api/src/main/scala/no/ndla/imageapi/model/api/Error.scala b/image-api/src/main/scala/no/ndla/imageapi/model/api/Error.scala index e949e40fe3..33fd501862 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/model/api/Error.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/model/api/Error.scala @@ -8,33 +8,13 @@ package no.ndla.imageapi.model.api import no.ndla.common.errors.{AccessDeniedException, FileTooBigException, ValidationException} -import no.ndla.common.{Clock, DateParser} - -import java.time.LocalDateTime +import no.ndla.common.Clock import no.ndla.imageapi.Props import no.ndla.imageapi.integration.DataSource -import no.ndla.imageapi.model.{ - ImageNotFoundException, - ImageStorageException, - ImportException, - InvalidUrlException, - ResultWindowTooLargeException -} +import no.ndla.imageapi.model._ import no.ndla.network.tapir.{AllErrors, TapirErrorHelpers} import no.ndla.search.{IndexNotFoundException, NdlaSearchException} import org.postgresql.util.PSQLException -import org.scalatra.servlet.SizeConstraintExceededException -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field - -@ApiModel(description = "Information about errors") -case class Error( - @(ApiModelProperty @field)(description = "Code stating the type of error") code: String, - @(ApiModelProperty @field)(description = "Description of the error") description: String, - @(ApiModelProperty @field)(description = "When the error occurred") occurredAt: String = - DateParser.dateToString(LocalDateTime.now(), withMillis = false) -) trait ErrorHelpers extends TapirErrorHelpers { this: Props with Clock with DataSource => @@ -52,8 +32,6 @@ trait ErrorHelpers extends TapirErrorHelpers { errorBody(GATEWAY_TIMEOUT, s.getMessage, 504) case rw: ResultWindowTooLargeException => errorBody(WINDOW_TOO_LARGE, rw.getMessage, 422) - case _: SizeConstraintExceededException => - errorBody(FILE_TOO_BIG, fileTooBigError, 413) case _: PSQLException => DataSource.connectToDatabase() errorBody(DATABASE_UNAVAILABLE, DATABASE_UNAVAILABLE_DESCRIPTION, 500) diff --git a/image-api/src/main/scala/no/ndla/imageapi/model/api/Image.scala b/image-api/src/main/scala/no/ndla/imageapi/model/api/Image.scala index d96a3e41ac..067668026d 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/model/api/Image.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/model/api/Image.scala @@ -7,14 +7,11 @@ package no.ndla.imageapi.model.api -import org.scalatra.swagger.annotations.ApiModel -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Url and size information about the image") +@description("Url and size information about the image") case class Image( - @(ApiModelProperty @field)(description = "The full url to where the image can be downloaded") url: String, - @(ApiModelProperty @field)(description = "The size of the image in bytes") size: Long, - @(ApiModelProperty @field)(description = "The mimetype of the image") contentType: String + @description("The full url to where the image can be downloaded") url: String, + @description("The size of the image in bytes") size: Long, + @description("The mimetype of the image") contentType: String ) diff --git a/image-api/src/main/scala/no/ndla/imageapi/model/api/NewImageFile.scala b/image-api/src/main/scala/no/ndla/imageapi/model/api/NewImageFile.scala index 58c3354974..a120398d80 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/model/api/NewImageFile.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/model/api/NewImageFile.scala @@ -7,14 +7,10 @@ package no.ndla.imageapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Url and size information about the image") +@description("Url and size information about the image") case class NewImageFile( - @(ApiModelProperty @field)(description = "The name of the file") fileName: String, - @(ApiModelProperty @field)( - description = "ISO 639-1 code that represents the language used in the audio" - ) language: Option[String] + @description("The name of the file") fileName: String, + @description("ISO 639-1 code that represents the language used in the audio") language: Option[String] ) diff --git a/image-api/src/main/scala/no/ndla/imageapi/model/api/ValidationError.scala b/image-api/src/main/scala/no/ndla/imageapi/model/api/ValidationError.scala index f92d7285ca..5cdb31a98c 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/model/api/ValidationError.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/model/api/ValidationError.scala @@ -9,16 +9,14 @@ package no.ndla.imageapi.model.api import no.ndla.common.errors.ValidationMessage -import java.time.LocalDateTime -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field +import java.time.LocalDateTime -@ApiModel(description = "Information about validation errors") +@description("Information about validation errors") case class ValidationError( - @(ApiModelProperty @field)(description = "Code stating the type of error") code: String, - @(ApiModelProperty @field)(description = "Description of the error") description: String = "Validation error", - @(ApiModelProperty @field)(description = "List of validation messages") messages: Seq[ValidationMessage], - @(ApiModelProperty @field)(description = "When the error occurred") occurredAt: LocalDateTime = LocalDateTime.now() + @description("Code stating the type of error") code: String, + @description("Description of the error") description: String = "Validation error", + @description("List of validation messages") messages: Seq[ValidationMessage], + @description("When the error occurred") occurredAt: LocalDateTime = LocalDateTime.now() ) diff --git a/image-api/src/test/scala/no/ndla/imageapi/TestEnvironment.scala b/image-api/src/test/scala/no/ndla/imageapi/TestEnvironment.scala index d2e9dea992..6a99437997 100644 --- a/image-api/src/test/scala/no/ndla/imageapi/TestEnvironment.scala +++ b/image-api/src/test/scala/no/ndla/imageapi/TestEnvironment.scala @@ -32,7 +32,6 @@ import no.ndla.imageapi.service.search.{ TagSearchService } import no.ndla.network.NdlaClient -import no.ndla.network.scalatra.{NdlaControllerBase, NdlaSwaggerSupport} import no.ndla.network.tapir.{NdlaMiddleware, Routes, Service} import no.ndla.search.{BaseIndexService, Elastic4sClient} import org.mockito.scalatest.MockitoSugar @@ -58,8 +57,6 @@ trait TestEnvironment with NdlaClient with InternController with BaseImageController - with NdlaSwaggerSupport - with NdlaControllerBase with ImageControllerV2 with ImageControllerV3 with RawController diff --git a/integration-tests/src/test/scala/no/ndla/integrationtests/searchapi/learningpathapi/LearningpathApiClientTest.scala b/integration-tests/src/test/scala/no/ndla/integrationtests/searchapi/learningpathapi/LearningpathApiClientTest.scala index 9b9613741d..446d65571b 100644 --- a/integration-tests/src/test/scala/no/ndla/integrationtests/searchapi/learningpathapi/LearningpathApiClientTest.scala +++ b/integration-tests/src/test/scala/no/ndla/integrationtests/searchapi/learningpathapi/LearningpathApiClientTest.scala @@ -7,6 +7,7 @@ package no.ndla.integrationtests.searchapi.learningpathapi +import cats.effect.{IO, unsafe} import enumeratum.Json4s import no.ndla.common.model.NDLADate import no.ndla.common.model.domain.draft.DraftStatus @@ -25,6 +26,7 @@ import org.json4s.Formats import org.json4s.ext.{EnumNameSerializer, JavaTimeSerializers} import org.testcontainers.containers.PostgreSQLContainer +import scala.concurrent.Future import scala.util.{Success, Try} class LearningpathApiClientTest @@ -63,11 +65,12 @@ class LearningpathApiClientTest var learningpathApi: learningpathapi.MainClass = null var learningpathApiServer: Server = null + var cancelFunc: () => Future[Unit] = null val learningpathApiBaseUrl = s"http://localhost:$learningpathApiPort" override def beforeAll(): Unit = { learningpathApi = new learningpathapi.MainClass(learningpathApiProperties) - learningpathApiServer = learningpathApi.startServer() + cancelFunc = IO { learningpathApi.run() }.unsafeRunCancelable()(unsafe.IORuntime.global) blockUntil(() => { import sttp.client3.quick._ val req = quickRequest.get(uri"$learningpathApiBaseUrl/health") @@ -78,7 +81,6 @@ class LearningpathApiClientTest override def afterAll(): Unit = { super.afterAll() - learningpathApiServer.stop() } private def setupLearningPaths() = { diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/ComponentRegistry.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/ComponentRegistry.scala index 80032269f7..520f5f0050 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/ComponentRegistry.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/ComponentRegistry.scala @@ -12,15 +12,13 @@ import com.zaxxer.hikari.HikariDataSource import no.ndla.common.Clock import no.ndla.common.configuration.BaseComponentRegistry import no.ndla.learningpathapi.controller.{ - HealthController, InternController, LearningpathControllerV2, - NdlaController, - StatsController + StatsController, + SwaggerDocControllerConfig } import no.ndla.learningpathapi.integration._ import no.ndla.learningpathapi.model.api.ErrorHelpers -import no.ndla.learningpathapi.model.domain.{DBLearningPath, DBLearningStep} import no.ndla.learningpathapi.repository.LearningPathRepositoryComponent import no.ndla.learningpathapi.service._ import no.ndla.learningpathapi.service.search.{SearchConverterServiceComponent, SearchIndexService, SearchService} @@ -34,17 +32,21 @@ import no.ndla.learningpathapi.validation.{ } import no.ndla.network.NdlaClient import no.ndla.network.clients.{FeideApiClient, RedisClient} -import no.ndla.network.scalatra.{NdlaControllerBase, NdlaSwaggerSupport} +import no.ndla.network.tapir.{ + NdlaMiddleware, + Routes, + Service, + SwaggerControllerConfig, + TapirErrorHelpers, + TapirHealthController +} import no.ndla.search.{BaseIndexService, Elastic4sClient} class ComponentRegistry(properties: LearningpathApiProperties) extends BaseComponentRegistry[LearningpathApiProperties] with LearningpathControllerV2 with InternController - with HealthController with StatsController - with NdlaSwaggerSupport - with NdlaControllerBase with LearningPathRepositoryComponent with ReadService with UpdateService @@ -72,42 +74,55 @@ class ComponentRegistry(properties: LearningpathApiProperties) with TextValidator with UrlValidator with ErrorHelpers - with LearningpathApiInfo - with DBLearningPath - with DBLearningStep - with NdlaController - with RedisClient { + with RedisClient + with Routes[Eff] + with NdlaMiddleware + with TapirErrorHelpers + with SwaggerControllerConfig + with SwaggerDocControllerConfig + with TapirHealthController { override val props: LearningpathApiProperties = properties override val migrator = new DBMigrator override val dataSource: HikariDataSource = DataSource.getHikariDataSource DataSource.connectToDatabase() - implicit val swagger: LearningpathSwagger = new LearningpathSwagger + lazy val learningPathRepository = new LearningPathRepository + lazy val readService = new ReadService + lazy val updateService = new UpdateService + lazy val searchConverterService = new SearchConverterService + lazy val searchService = new SearchService + lazy val searchIndexService = new SearchIndexService + lazy val converterService = new ConverterService + lazy val clock = new SystemClock + lazy val taxonomyApiClient = new TaxonomyApiClient + lazy val ndlaClient = new NdlaClient + lazy val imageApiClient = new ImageApiClient + lazy val feideApiClient = new FeideApiClient + lazy val languageValidator = new LanguageValidator + lazy val titleValidator = new TitleValidator + lazy val learningPathValidator = new LearningPathValidator + lazy val learningStepValidator = new LearningStepValidator + var e4sClient: NdlaE4sClient = Elastic4sClientFactory.getClient(props.SearchServer) + lazy val searchApiClient = new SearchApiClient + lazy val oembedProxyClient = new OembedProxyClient + lazy val redisClient = new RedisClient(props.RedisHost, props.RedisPort) + lazy val myndlaApiClient = new MyNDLAApiClient - lazy val learningPathRepository = new LearningPathRepository - lazy val readService = new ReadService - lazy val updateService = new UpdateService - lazy val searchConverterService = new SearchConverterService - lazy val searchService = new SearchService - lazy val searchIndexService = new SearchIndexService - lazy val converterService = new ConverterService - lazy val clock = new SystemClock lazy val learningpathControllerV2 = new LearningpathControllerV2 lazy val internController = new InternController lazy val statsController = new StatsController - lazy val resourcesApp = new ResourcesApp - lazy val taxonomyApiClient = new TaxonomyApiClient - lazy val ndlaClient = new NdlaClient - lazy val imageApiClient = new ImageApiClient - lazy val feideApiClient = new FeideApiClient - lazy val healthController = new HealthController - lazy val languageValidator = new LanguageValidator - lazy val titleValidator = new TitleValidator - lazy val learningPathValidator = new LearningPathValidator - lazy val learningStepValidator = new LearningStepValidator - var e4sClient: NdlaE4sClient = Elastic4sClientFactory.getClient(props.SearchServer) - lazy val searchApiClient = new SearchApiClient - lazy val oembedProxyClient = new OembedProxyClient - lazy val redisClient = new RedisClient(props.RedisHost, props.RedisPort) - lazy val myndlaApiClient = new MyNDLAApiClient + lazy val healthController = new TapirHealthController[Eff] + + private val swagger = new SwaggerController[Eff]( + List[Service[Eff]]( + learningpathControllerV2, + internController, + statsController, + healthController + ), + SwaggerDocControllerConfig.swaggerInfo + ) + + override val services: List[Service[Eff]] = swagger.getServices() + } diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/LearningpathSwagger.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/LearningpathSwagger.scala deleted file mode 100644 index f904f79384..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/LearningpathSwagger.scala +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi - -import org.scalatra.ScalatraServlet -import org.scalatra.swagger._ - -class ResourcesApp(implicit val swagger: Swagger) extends ScalatraServlet with NativeSwaggerBase { - get("/") { - renderSwagger2(swagger.docs.toList) - }: Unit -} - -trait LearningpathApiInfo { - this: Props => - - object LearningpathApiInfo { - - private val contactInfo: ContactInfo = ContactInfo( - props.ContactName, - props.ContactUrl, - props.ContactEmail - ) - - private val licenseInfo: LicenseInfo = LicenseInfo( - "GPL v3.0", - "http://www.gnu.org/licenses/gpl-3.0.en.html" - ) - - val apiInfo: ApiInfo = ApiInfo( - "Learningpath API", - "Services for accessing learningpaths", - props.TermsUrl, - contactInfo, - licenseInfo - ) - } - - class LearningpathSwagger extends Swagger("2.0", "1.0", LearningpathApiInfo.apiInfo) { - addAuthorization( - OAuth(List(), List(ImplicitGrant(LoginEndpoint(props.Auth0LoginEndpoint), "access_token"))) - ) - } -} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/Main.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/Main.scala index 24fad3dc47..2b8c9e88ac 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/Main.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/Main.scala @@ -15,6 +15,6 @@ object Main { setPropsFromEnv() val props = new LearningpathApiProperties val mainClass = new MainClass(props) - mainClass.start() + mainClass.run() } } diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/MainClass.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/MainClass.scala index 3b0abf1ecd..d4b6df6a39 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/MainClass.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/MainClass.scala @@ -8,32 +8,30 @@ package no.ndla.learningpathapi -import com.typesafe.scalalogging.StrictLogging -import no.ndla.network.scalatra.NdlaScalatraServer -import org.eclipse.jetty.server.Server +import no.ndla.common.Warmup +import no.ndla.network.tapir.NdlaTapirMain -class MainClass(props: LearningpathApiProperties) extends StrictLogging { +class MainClass(override val props: LearningpathApiProperties) extends NdlaTapirMain[Eff] { val componentRegistry = new ComponentRegistry(props) - def startServer(): Server = { - new NdlaScalatraServer[LearningpathApiProperties, ComponentRegistry]( - "no.ndla.learningpathapi.ScalatraBootstrap", - componentRegistry, { - logger.info("Starting the db migration...") - val startDBMillis = System.currentTimeMillis() - componentRegistry.migrator.migrate(): Unit - logger.info(s"Done db migration, took ${System.currentTimeMillis() - startDBMillis}ms") - }, - warmupRequest => { - warmupRequest("/learningpath-api/v2/learningpaths", Map("query" -> "norge", "fallback" -> "true")) - warmupRequest("/learningpath-api/v2/learningpaths/1", Map.empty) - warmupRequest("/health", Map.empty) - } - ) + private def warmupRequest = (path: String, options: Map[String, String]) => + Warmup.warmupRequest(props.ApplicationPort, path, options) + + override def warmup(): Unit = { + warmupRequest("/learningpath-api/v2/learningpaths", Map("query" -> "norge", "fallback" -> "true")) + warmupRequest("/learningpath-api/v2/learningpaths/1", Map.empty) + warmupRequest("/health", Map.empty) + + componentRegistry.healthController.setWarmedUp() } - def start(): Unit = { - val server = startServer() - server.join() + override def beforeStart(): Unit = { + logger.info("Starting the db migration...") + val startDBMillis = System.currentTimeMillis() + componentRegistry.migrator.migrate(): Unit + logger.info(s"Done db migration, took ${System.currentTimeMillis() - startDBMillis}ms") } + + override def startServer(name: String, port: Int)(warmupFunc: => Unit): Unit = + componentRegistry.Routes.startJdkServer(name, port)(warmupFunc) } diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/ScalatraBootstrap.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/ScalatraBootstrap.scala deleted file mode 100644 index 59cf419670..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/ScalatraBootstrap.scala +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi - -import no.ndla.network.scalatra.NdlaScalatraBootstrapBase -import javax.servlet.ServletContext - -class ScalatraBootstrap extends NdlaScalatraBootstrapBase[ComponentRegistry] { - override def ndlaInit(context: ServletContext, componentRegistry: ComponentRegistry): Unit = { - context.mount(componentRegistry.learningpathControllerV2, "/learningpath-api/v2/learningpaths", "learningpaths_v2") - context.mount(componentRegistry.internController, "/intern") - context.mount(componentRegistry.resourcesApp, "/learningpath-api/api-docs") - context.mount(componentRegistry.healthController, "/health") - context.mount(componentRegistry.statsController, "/learningpath-api/v1/stats", "stats") - } -} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/HealthController.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/HealthController.scala deleted file mode 100644 index d009cee46c..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/HealthController.scala +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.controller - -import no.ndla.network.scalatra.BaseHealthController - -trait HealthController { - val healthController: HealthController - - class HealthController extends BaseHealthController {} -} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/InternController.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/InternController.scala index 552c837ec0..d8e219a3ca 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/InternController.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/InternController.scala @@ -8,23 +8,22 @@ package no.ndla.learningpathapi.controller -import enumeratum.Json4s -import no.ndla.learningpathapi.Props +import cats.implicits.catsSyntaxEitherId +import no.ndla.learningpathapi.model.api.{ErrorHelpers, LearningPathDomainDump, LearningPathSummaryV2} +import no.ndla.learningpathapi.{Eff, Props} import no.ndla.learningpathapi.model.domain -import no.ndla.learningpathapi.model.domain._ import no.ndla.learningpathapi.repository.LearningPathRepositoryComponent import no.ndla.learningpathapi.service.search.{SearchIndexService, SearchService} import no.ndla.learningpathapi.service.{ReadService, UpdateService} -import no.ndla.common.errors.AccessDeniedException -import no.ndla.common.model.NDLADate -import no.ndla.common.model.domain.learningpath.EmbedType -import no.ndla.network.AuthUser -import org.json4s.Formats -import org.json4s.ext.{EnumNameSerializer, JavaTimeSerializers} -import org.scalatra._ -import org.scalatra.swagger.Swagger +import no.ndla.network.tapir.NoNullJsonPrinter.jsonBody +import no.ndla.network.tapir.Service +import no.ndla.network.tapir.TapirErrors.errorOutputsFor +import sttp.model.StatusCode +import sttp.tapir._ +import sttp.tapir.generic.auto._ +import sttp.tapir.model.{CommaSeparated, Delimited} +import sttp.tapir.server.ServerEndpoint -import javax.servlet.http.HttpServletRequest import scala.util.{Failure, Success} trait InternController { @@ -33,107 +32,124 @@ trait InternController { with LearningPathRepositoryComponent with ReadService with UpdateService - with NdlaController - with Props => + with Props + with ErrorHelpers => val internController: InternController - class InternController(implicit val swagger: Swagger) extends NdlaController { - protected val applicationDescription = "API for accessing internal functionality in learningpath API" - protected implicit override val jsonFormats: Formats = - org.json4s.DefaultFormats + - new EnumNameSerializer(LearningPathStatus) + - new EnumNameSerializer(LearningPathVerificationStatus) + - new EnumNameSerializer(StepType) + - Json4s.serializer(StepStatus) + - new EnumNameSerializer(EmbedType) ++ - JavaTimeSerializers.all + - NDLADate.Json4sSerializer - - def requireClientId(implicit request: HttpServletRequest): String = { - AuthUser.getClientId match { - case Some(clientId) => clientId - case None => { - logger.warn(s"Request made to ${request.getRequestURI} without clientId") - throw AccessDeniedException("You do not have access to the requested resource.") + class InternController extends Service[Eff] { + override val prefix = "intern" + override val enableSwagger = false + private val stringInternalServerError = statusCode(StatusCode.InternalServerError).and(stringBody) + import ErrorHelpers._ + + override val endpoints: List[ServerEndpoint[Any, Eff]] = List( + getByExternalId, + postIndex, + deleteIndex, + dumpLearningpaths, + dumpSingleLearningPath, + postLearningPathDump, + containsArticle + ) + + def getByExternalId: ServerEndpoint[Any, Eff] = endpoint.get + .in("id" / path[String]("external_id")) + .out(stringBody) + .errorOut(errorOutputsFor(404)) + .serverLogicPure { externalId => + learningPathRepository.getIdFromExternalId(externalId) match { + case Some(id) => id.toString.asRight + case None => notFound.asLeft } - } - } - get("/id/:external_id") { - val externalId = params("external_id") - learningPathRepository.getIdFromExternalId(externalId) match { - case Some(id) => id.toString - case None => NotFound() } - }: Unit - post("/index") { - val numShards = intOrNone("numShards") - searchIndexService.indexDocuments(numShards) match { - case Success(reindexResult) => - val result = - s"Completed indexing of ${reindexResult.totalIndexed} documents in ${reindexResult.millisUsed} ms." - logger.info(result) - Ok(result) - case Failure(f) => - logger.warn(f.getMessage, f) - InternalServerError(f.getMessage) + def postIndex: ServerEndpoint[Any, Eff] = endpoint.post + .in("index") + .in(query[Option[Int]]("numShards")) + .out(stringBody) + .errorOut(stringInternalServerError) + .serverLogicPure { numShards => + searchIndexService.indexDocuments(numShards) match { + case Success(reindexResult) => + val result = + s"Completed indexing of ${reindexResult.totalIndexed} documents in ${reindexResult.millisUsed} ms." + logger.info(result) + result.asRight + case Failure(f) => + logger.warn(f.getMessage, f) + f.getMessage.asLeft + } } - }: Unit - delete("/index") { - def pluralIndex(n: Int) = if (n == 1) "1 index" else s"$n indexes" - searchIndexService - .findAllIndexes(props.SearchIndex) - .map(indexes => { - indexes.map(index => { - logger.info(s"Deleting index $index") - searchIndexService.deleteIndexWithName(Option(index)) - }) - }) match { - case Failure(ex) => InternalServerError(ex.getMessage) - case Success(deleteResults) => - val (errors, successes) = deleteResults.partition(_.isFailure) - if (errors.nonEmpty) { - val message = s"Failed to delete ${pluralIndex(errors.length)}: " + - s"${errors.map(_.failed.get.getMessage).mkString(", ")}. " + - s"${pluralIndex(successes.length)} were deleted successfully." - InternalServerError(body = message) - } else { - Ok(body = s"Deleted ${pluralIndex(successes.length)}") - } + def deleteIndex: ServerEndpoint[Any, Eff] = endpoint.delete + .in("index") + .out(stringBody) + .errorOut(stringInternalServerError) + .serverLogicPure { _ => + def pluralIndex(n: Int) = if (n == 1) "1 index" else s"$n indexes" + searchIndexService + .findAllIndexes(props.SearchIndex) + .map(indexes => { + indexes.map(index => { + logger.info(s"Deleting index $index") + searchIndexService.deleteIndexWithName(Option(index)) + }) + }) match { + case Failure(ex) => ex.getMessage.asLeft + case Success(deleteResults) => + val (errors, successes) = deleteResults.partition(_.isFailure) + if (errors.nonEmpty) { + val message = s"Failed to delete ${pluralIndex(errors.length)}: " + + s"${errors.map(_.failed.get.getMessage).mkString(", ")}. " + + s"${pluralIndex(successes.length)} were deleted successfully." + message.asLeft + } else { + s"Deleted ${pluralIndex(successes.length)}".asRight + } + } } - }: Unit - - get("/dump/learningpath/?") { - val pageNo = intOrDefault("page", 1) - val pageSize = intOrDefault("page-size", 250) - val onlyIncludePublished = booleanOrDefault("only-published", true) - - readService.getLearningPathDomainDump(pageNo, pageSize, onlyIncludePublished) - }: Unit - get("/dump/learningpath/:learningpath_id") { - val learningpathId = long("learningpath_id") - learningPathRepository.withId(learningpathId) match { - case Some(value) => Ok(value) - case None => NotFound() + def dumpLearningpaths: ServerEndpoint[Any, Eff] = endpoint.get + .in("dump" / "learningpath") + .in(query[Int]("page").default(1)) + .in(query[Int]("page-size").default(250)) + .in(query[Boolean]("only-published").default(true)) + .out(jsonBody[LearningPathDomainDump]) + .serverLogicPure { case (pageNo, pageSize, onlyIncludePublished) => + readService.getLearningPathDomainDump(pageNo, pageSize, onlyIncludePublished).asRight } - }: Unit - post("/dump/learningpath/?") { - val dumpToInsert = extract[domain.LearningPath](request.body) - updateService.insertDump(dumpToInsert) - }: Unit - - get("/containsArticle") { - val paths = paramAsListOfString("paths") + def dumpSingleLearningPath: ServerEndpoint[Any, Eff] = endpoint.get + .in("dump" / "learningpath" / path[Long]("learningpath_id")) + .out(jsonBody[domain.LearningPath]) + .errorOut(errorOutputsFor(404)) + .serverLogicPure { learningpathId => + learningPathRepository.withId(learningpathId) match { + case Some(value) => value.asRight + case None => notFound.asLeft + } + } - searchService.containsPath(paths) match { - case Success(result) => result.results - case Failure(ex) => errorHandler(ex) + def postLearningPathDump: ServerEndpoint[Any, Eff] = endpoint.post + .in("dump" / "learningpath") + .in(jsonBody[domain.LearningPath]) + .out(jsonBody[domain.LearningPath]) + .errorOut(errorOutputsFor(404)) + .serverLogicPure { dumpToInsert => + updateService.insertDump(dumpToInsert).asRight } - }: Unit + def containsArticle: ServerEndpoint[Any, Eff] = endpoint.get + .in("containsArticle") + .in(query[CommaSeparated[String]]("paths").default(Delimited[",", String](List.empty))) + .out(jsonBody[Seq[LearningPathSummaryV2]]) + .errorOut(errorOutputsFor(404)) + .serverLogicPure { paths => + searchService.containsPath(paths.values) match { + case Success(result) => result.results.asRight + case Failure(ex) => returnLeftError(ex) + } + } } } diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2.scala index 4dab51cb9e..5077c994ff 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2.scala @@ -8,10 +8,11 @@ package no.ndla.learningpathapi.controller -import no.ndla.common.model.NDLADate +import cats.implicits.catsSyntaxEitherId import no.ndla.common.model.api.{Author, License} +import no.ndla.language.Language import no.ndla.language.Language.AllLanguages -import no.ndla.learningpathapi.Props +import no.ndla.learningpathapi.{Eff, Props} import no.ndla.learningpathapi.integration.TaxonomyApiClient import no.ndla.learningpathapi.model.api._ import no.ndla.learningpathapi.model.domain @@ -22,11 +23,15 @@ import no.ndla.learningpathapi.service.{ConverterService, ReadService, UpdateSer import no.ndla.learningpathapi.validation.LanguageValidator import no.ndla.mapping import no.ndla.mapping.LicenseDefinition -import no.ndla.network.scalatra.NdlaSwaggerSupport -import org.json4s.ext.JavaTimeSerializers -import org.json4s.{DefaultFormats, Formats} -import org.scalatra.swagger._ -import org.scalatra.{Created, NoContent, NotFound, Ok} +import no.ndla.network.tapir.NoNullJsonPrinter.jsonBody +import no.ndla.network.tapir.TapirErrors.errorOutputsFor +import no.ndla.network.tapir.auth.TokenUser +import no.ndla.network.tapir.{DynamicHeaders, Service} +import sttp.model.StatusCode +import sttp.tapir._ +import sttp.tapir.generic.auto._ +import sttp.tapir.model.{CommaSeparated, Delimited} +import sttp.tapir.server.ServerEndpoint import scala.util.{Failure, Success, Try} @@ -39,14 +44,13 @@ trait LearningpathControllerV2 { with ConverterService with TaxonomyApiClient with SearchConverterServiceComponent - with NdlaController with Props - with ErrorHelpers - with NdlaSwaggerSupport => + with ErrorHelpers => val learningpathControllerV2: LearningpathControllerV2 - class LearningpathControllerV2(implicit val swagger: Swagger) extends NdlaController with NdlaSwaggerSupport { + class LearningpathControllerV2 extends Service[Eff] { + import ErrorHelpers._ import props.{ DefaultLanguage, ElasticSearchIndexMaxResultWindow, @@ -54,73 +58,63 @@ trait LearningpathControllerV2 { InitialScrollContextKeywords } - protected implicit override val jsonFormats: Formats = - DefaultFormats ++ JavaTimeSerializers.all + NDLADate.Json4sSerializer + override val serviceName: String = "learningpaths" + override val prefix: EndpointInput[Unit] = "learningpath-api" / "v2" / serviceName - protected val applicationDescription = "API for accessing Learningpaths from ndla.no." - - // Additional models used in error responses - registerModel[ValidationError]() - registerModel[Error]() - - val response400: ResponseMessage = ResponseMessage(400, "Validation Error", Some("ValidationError")) - val response403: ResponseMessage = ResponseMessage(403, "Access not granted", Some("Error")) - val response404: ResponseMessage = ResponseMessage(404, "Not found", Some("Error")) - val response500: ResponseMessage = ResponseMessage(500, "Unknown error", Some("Error")) - val response502: ResponseMessage = ResponseMessage(502, "Remote error", Some("Error")) - - private val articleId = - Param[String]("article_id", "Id of the article to search with") - private val correlationId = - Param[Option[String]]("X-Correlation-ID", "User supplied correlation-id. May be omitted.") - private val query = - Param[Option[String]]("query", "Return only Learningpaths with content matching the specified query.") + private val pathArticleId = + path[Long]("article_id").description("Id of the article to search with") + private val queryParam = + query[Option[String]]("query").description("Return only Learningpaths with content matching the specified query.") private val language = - Param[Option[String]]("language", "The ISO 639-1 language code describing language.") - private val sort = Param[Option[String]]( - "sort", + query[String]("language") + .description("The ISO 639-1 language code describing language.") + .default(Language.AllLanguages) + private val sort = query[Option[String]]("sort").description( s"""The sorting used on results. The following are supported: ${Sort.all.mkString(", ")}. Default is by -relevance (desc) when query is set, and title (asc) when query is empty.""".stripMargin ) - private val pageNo = - Param[Option[Int]]("page", "The page number of the search hits to display.") - private val pageSize = - Param[Option[Int]]("page-size", "The number of search hits to display for each page.") - private val learningpathId = - Param[String]("learningpath_id", "Id of the learningpath.") - private val learningstepId = - Param[String]("learningstep_id", "Id of the learningstep.") - private val tag = Param[Option[String]]("tag", "Return only Learningpaths that are tagged with this exact tag.") - private val learningpathIds = Param[Option[String]]( - "ids", - "Return only Learningpaths that have one of the provided ids. To provide multiple ids, separate by comma (,)." - ) - private val licenseFilter = - Param[Option[String]]( - "filter", - "Query for filtering licenses. Only licenses containing filter-string are returned." + private val pageNo = query[Option[Int]]("page") + .description("The page number of the search hits to display.") + private val pageSize = query[Option[Int]]("page-size") + .description("The number of search hits to display for each page.") + private val pathLearningpathId = + path[Long]("learningpath_id").description("Id of the learningpath.") + private val pathLearningstepId = + path[Long]("learningstep_id").description("Id of the learningstep.") + private val tag = + query[Option[String]]("tag").description("Return only Learningpaths that are tagged with this exact tag.") + private val learningpathIds = query[CommaSeparated[Long]]("ids") + .description( + "Return only Learningpaths that have one of the provided ids. To provide multiple ids, separate by comma (,)." ) - private val fallback = Param[Option[Boolean]]("fallback", "Fallback to existing language if language is specified.") - private val createResourceIfMissing = - Param[Option[Boolean]]("create-if-missing", "Create taxonomy resource if missing for learningPath") - private val learningPathStatus = - Param[String]("STATUS", "Status of LearningPaths") - private val scrollId = Param[Option[String]]( - "search-context", - s"""A unique string obtained from a search you want to keep scrolling in. To obtain one from a search, provide one of the following values: ${InitialScrollContextKeywords - .mkString("[", ",", "]")}. - |When scrolling, the parameters from the initial search is used, except in the case of '${this.language.paramName}' and '${this.fallback.paramName}'. + .default(Delimited[",", Long](List.empty)) + private val licenseFilter = + query[Option[String]]("filter") + .description("Query for filtering licenses. Only licenses containing filter-string are returned.") + private val fallback = query[Boolean]("fallback") + .description("Fallback to existing language if language is specified.") + .default(false) + private val createResourceIfMissing = query[Boolean]("create-if-missing") + .description("Create taxonomy resource if missing for learningPath") + .default(false) + private val learningPathStatus = path[String]("STATUS").description("Status of LearningPaths") + private val scrollId = query[Option[String]]("search-context") + .description( + s"""A unique string obtained from a search you want to keep scrolling in. To obtain one from a search, provide one of the following values: ${InitialScrollContextKeywords + .mkString("[", ",", "]")}. + |When scrolling, the parameters from the initial search is used, except in the case of '${this.language.name}' and '${this.fallback.name}'. |This value may change between scrolls. Always use the one in the latest scroll result (The context, if unused, dies after $ElasticSearchScrollKeepAlive). - |If you are not paginating past $ElasticSearchIndexMaxResultWindow hits, you can ignore this and use '${this.pageNo.paramName}' and '${this.pageSize.paramName}' instead. + |If you are not paginating past $ElasticSearchIndexMaxResultWindow hits, you can ignore this and use '${this.pageNo.name}' and '${this.pageSize.name}' instead. |""".stripMargin - ) - private val verificationStatus = - Param[Option[String]]("verificationStatus", "Return only learning paths that have this verification status.") - private val ids = Param[Option[Seq[Long]]]( - "ids", - "Return only learningpaths that have one of the provided ids. To provide multiple ids, separate by comma (,)." - ) + ) + private val verificationStatus = query[Option[String]]("verificationStatus") + .description("Return only learning paths that have this verification status.") + private val ids = query[CommaSeparated[Long]]("ids") + .description( + "Return only learningpaths that have one of the provided ids. To provide multiple ids, separate by comma (,)." + ) + .default(Delimited[",", Long](List.empty)) /** Does a scroll with [[SearchService]] If no scrollId is specified execute the function @orFunction in the second * parameter list. @@ -130,20 +124,20 @@ trait LearningpathControllerV2 { * @return * A Try with scroll result, or the return of the orFunction (Usually a try with a search result). */ - private def scrollSearchOr(scrollId: Option[String], language: String)(orFunction: => Any): Any = { + private def scrollSearchOr(scrollId: Option[String], language: String)( + orFunction: => Try[(SearchResultV2, DynamicHeaders)] + ): Try[(SearchResultV2, DynamicHeaders)] = scrollId match { case Some(scroll) if !InitialScrollContextKeywords.contains(scroll) => searchService.scroll(scroll, language) match { case Success(scrollResult) => - val responseHeader = scrollResult.scrollId - .map(i => this.scrollId.paramName -> i) - .toMap - Ok(searchConverterService.asApiSearchResult(scrollResult), headers = responseHeader) - case Failure(ex) => errorHandler(ex) + val body = searchConverterService.asApiSearchResult(scrollResult) + val headers = DynamicHeaders.fromMaybeValue("search-context", scrollResult.scrollId) + Success((body, headers)) + case Failure(ex) => Failure(ex) } case _ => orFunction } - } private def search( query: Option[String], @@ -192,749 +186,561 @@ trait LearningpathControllerV2 { searchService.matchingQuery(settings) match { case Success(searchResult) => - val responseHeader = - searchResult.scrollId.map(i => this.scrollId.paramName -> i).toMap - Ok(searchConverterService.asApiSearchResult(searchResult), headers = responseHeader) - case Failure(ex) => errorHandler(ex) + val scrollHeader = DynamicHeaders.fromMaybeValue("search-context", searchResult.scrollId) + val output = searchConverterService.asApiSearchResult(searchResult) + Success((output, scrollHeader)) + case Failure(ex) => Failure(ex) } } - get( - "/", - operation( - apiOperation[SearchResultV2]("getLearningpaths") - .summary("Find public learningpaths") - .description("Show public learningpaths.") - .parameters( - asHeaderParam(correlationId), - asQueryParam(query), - asQueryParam(tag), - asQueryParam(learningpathIds), - asQueryParam(language), - asQueryParam(pageNo), - asQueryParam(pageSize), - asQueryParam(sort), - asQueryParam(fallback), - asQueryParam(scrollId), - asQueryParam(verificationStatus) - ) - .responseMessages(response400, response500) - .authorizations("oauth2") - ) - ) { - val scrollId = paramOrNone(this.scrollId.paramName) - val language = paramOrDefault(this.language.paramName, AllLanguages) - - scrollSearchOr(scrollId, language) { - val query = paramOrNone(this.query.paramName) - val tag = paramOrNone(this.tag.paramName) - val idList = paramAsListOfLong(this.learningpathIds.paramName) - val sort = paramOrNone(this.sort.paramName) - val pageSize = paramOrNone(this.pageSize.paramName).flatMap(ps => Try(ps.toInt).toOption) - val page = paramOrNone(this.pageNo.paramName).flatMap(idx => Try(idx.toInt).toOption) - val fallback = - booleanOrDefault(this.fallback.paramName, default = false) - val verificationStatus = paramOrNone(this.verificationStatus.paramName) - val shouldScroll = paramOrNone(this.scrollId.paramName).exists(InitialScrollContextKeywords.contains) - - search(query, language, tag, idList, sort, pageSize, page, fallback, verificationStatus, shouldScroll) + override val endpoints: List[ServerEndpoint[Any, Eff]] = List( + getLearningpaths, + postSearch, + getTags, + getLicenses, + getMyLearningpaths, + getContributors, + getLearningpathsByIds, + getLearningpath, + getLearningpathStatus, + getLearningStepsInTrash, + getLearningsteps, + getLearningStep, + fetchLearningPathContainingArticle, + getLearningStepStatus, + addLearningpath, + copyLearningpath, + updateLearningPath, + addLearningStep, + updateLearningStep, + updatedLearningstepSeqNo, + updateLearningStepStatus, + updateLearningPathStatus, + withStatus, + deleteLearningpath, + deleteLearningStep, + updateLearningPathTaxonomy + ) + + def getLearningpaths: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Find public learningpaths") + .description("Show public learningpaths.") + .in(queryParam) + .in(tag) + .in(learningpathIds) + .in(language) + .in(pageNo) + .in(pageSize) + .in(sort) + .in(fallback) + .in(scrollId) + .in(verificationStatus) + .out(jsonBody[SearchResultV2]) + .out(EndpointOutput.derived[DynamicHeaders]) + .errorOut(errorOutputsFor(400)) + .serverLogicPure { + case (query, tag, idList, language, pageNo, pageSize, sortStr, fallback, scrollId, verificationStatus) => + scrollSearchOr(scrollId, language) { + val shouldScroll = scrollId.exists(InitialScrollContextKeywords.contains) + search( + query, + language, + tag, + idList.values, + sortStr, + pageSize, + pageNo, + fallback, + verificationStatus, + shouldScroll + ) + }.handleErrorsOrOk } - }: Unit - - post( - "/search/", - operation( - apiOperation[List[SearchResultV2]]("searchArticles") - .summary("Find public learningpaths") - .description("Show public learningpaths") - .parameters( - asHeaderParam(correlationId), - bodyParam[SearchParams], - asQueryParam(scrollId) + + def postSearch: ServerEndpoint[Any, Eff] = endpoint.post + .summary("Find public learningpaths") + .description("Show public learningpaths") + .in("search") + .in(jsonBody[SearchParams]) + .errorOut(errorOutputsFor(400)) + .out(jsonBody[SearchResultV2]) + .out(EndpointOutput.derived[DynamicHeaders]) + .serverLogicPure { searchParams => + scrollSearchOr(searchParams.scrollId, searchParams.language.getOrElse(AllLanguages)) { + val shouldScroll = searchParams.scrollId.exists(InitialScrollContextKeywords.contains) + search( + query = searchParams.query, + searchLanguage = searchParams.language.getOrElse(AllLanguages), + tag = searchParams.tag, + idList = searchParams.ids.getOrElse(List.empty), + sort = searchParams.sort, + pageSize = searchParams.pageSize, + page = searchParams.page, + fallback = searchParams.fallback.getOrElse(false), + verificationStatus = searchParams.verificationStatus, + shouldScroll = shouldScroll ) - .authorizations("oauth2") - .responseMessages(response400, response500) - ) - ) { - val searchParams = extract[SearchParams](request.body) - val language = searchParams.language.getOrElse(AllLanguages) - - scrollSearchOr(searchParams.scrollId, language) { - val query = searchParams.query - val tag = searchParams.tag - val idList = searchParams.ids - val sort = searchParams.sort - val pageSize = searchParams.pageSize - val page = searchParams.page - val fallback = searchParams.fallback.getOrElse(false) - val verificationStatus = searchParams.verificationStatus - val shouldScroll = searchParams.scrollId.exists(InitialScrollContextKeywords.contains) - - search(query, language, tag, idList, sort, pageSize, page, fallback, verificationStatus, shouldScroll) + }.handleErrorsOrOk } - }: Unit - - get( - "/ids/", - operation( - apiOperation[List[LearningPathV2]]("getLearningpathsByIds") - .summary("Fetch learningpaths that matches ids parameter.") - .description("Returns learningpaths that matches ids parameter.") - .parameters( - asHeaderParam(correlationId), - asQueryParam(ids), - asQueryParam(fallback), - asQueryParam(language), - asQueryParam(pageSize), - asQueryParam(pageNo) - ) - .responseMessages(response403, response500) - .authorizations("oauth2") - ) - ) { - doIfAccessTrue(user => user.isNdla) { userInfo => - val idList = paramAsListOfLong(this.ids.paramName) - val fallback = booleanOrDefault(this.fallback.paramName, default = true) - val language = paramOrDefault(this.language.paramName, AllLanguages) - val pageSize = intOrDefault(this.pageSize.paramName, props.DefaultPageSize) match { - case tooSmall if tooSmall < 1 => props.DefaultPageSize - case x => x - } - val page = intOrDefault(this.pageNo.paramName, 1) match { - case tooSmall if tooSmall < 1 => 1 - case x => x + + def getLearningpathsByIds: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Fetch learningpaths that matches ids parameter.") + .description("Returns learningpaths that matches ids parameter.") + .in("ids") + .in(ids) + .in(fallback) + .in(language) + .in(pageSize) + .in(pageNo) + .errorOut(errorOutputsFor(400, 401, 403)) + .out(jsonBody[Seq[LearningPathV2]]) + .requirePermission() + .serverLogicPure { user => + { case (idList, fallback, language, pageSizeQ, pageNoQ) => + if (!user.isNdla) { + forbidden.asLeft + } else { + val pageSize = pageSizeQ.getOrElse(props.DefaultPageSize) match { + case tooSmall if tooSmall < 1 => props.DefaultPageSize + case x => x + } + val page = pageNoQ.getOrElse(1) match { + case tooSmall if tooSmall < 1 => 1 + case x => x + } + readService.withIdV2List(idList.values, language, fallback, page, pageSize, user) match { + case Failure(ex) => returnLeftError(ex) + case Success(articles) => articles.asRight + } + } } - readService.withIdV2List(idList, language, fallback, page, pageSize, userInfo) match { - case Failure(ex) => errorHandler(ex) - case Success(articles) => Ok(articles) + } + + def getLearningpath: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Fetch details about the specified learningpath") + .description("Shows all information about the specified learningpath.") + .in(pathLearningpathId) + .in(language) + .in(fallback) + .out(jsonBody[LearningPathV2]) + .errorOut(errorOutputsFor(401, 403, 404)) + .withOptionalUser + .serverLogicPure { maybeUser => + { case (id, language, fallback) => + val user = maybeUser.getOrElse(TokenUser.PublicUser) + readService.withIdV2(id, language, fallback, user).handleErrorsOrOk } } - }: Unit - - get( - "/:learningpath_id", - operation( - apiOperation[LearningPathV2]("getLearningpath") - .summary("Fetch details about the specified learningpath") - .description("Shows all information about the specified learningpath.") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - asQueryParam(language), - asQueryParam(fallback) - ) - .responseMessages(response403, response404, response500) - .authorizations("oauth2") - ) - ) { - val language = - paramOrDefault(this.language.paramName, AllLanguages) - val id = long(this.learningpathId.paramName) - val userInfo = UserInfo.getUserOrPublic - val fallback = booleanOrDefault(this.fallback.paramName, default = false) - - readService.withIdV2(id, language, fallback, userInfo) match { - case Success(lp) => Ok(lp) - case Failure(ex) => errorHandler(ex) + + def getLearningpathStatus: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Show status information for the learningpath") + .description("Shows publishingstatus for the learningpath") + .in(pathLearningpathId / "status") + .out(jsonBody[LearningPathStatus]) + .errorOut(errorOutputsFor(401, 403, 404)) + .withOptionalUser + .serverLogicPure { + maybeUser => + { id => + readService.statusFor(id, maybeUser.getOrElse(TokenUser.PublicUser)).handleErrorsOrOk + } } - }: Unit - - get( - "/:learningpath_id/status/", - operation( - apiOperation[LearningPathStatus]("getLearningpathStatus") - .summary("Show status information for the learningpath") - .description("Shows publishingstatus for the learningpath") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId) - ) - .responseMessages(response403, response404, response500) - .authorizations("oauth2") - ) - ) { - val id = long(this.learningpathId.paramName) - readService.statusFor(id, UserInfo.getUserOrPublic) match { - case Success(status) => Ok(status) - case Failure(ex) => errorHandler(ex) + + def getLearningsteps: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Fetch learningsteps for given learningpath") + .description("Show all learningsteps for given learningpath id") + .in(pathLearningpathId / "learningsteps") + .in(fallback) + .in(language) + .out(jsonBody[LearningStepContainerSummary]) + .errorOut(errorOutputsFor(401, 403, 404)) + .withOptionalUser + .serverLogicPure { maybeUser => + { case (id, fallback, language) => + readService + .learningstepsForWithStatusV2( + id, + StepStatus.ACTIVE, + language, + fallback, + maybeUser.getOrElse(TokenUser.PublicUser) + ) + .handleErrorsOrOk + } } - }: Unit - - get( - "/:learningpath_id/learningsteps/", - operation( - apiOperation[List[LearningStepContainerSummary]]("getLearningsteps") - .summary("Fetch learningsteps for given learningpath") - .description("Show all learningsteps for given learningpath id") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - asQueryParam(language), - asQueryParam(fallback) - ) - .responseMessages(response403, response404, response500) - .authorizations("oauth2") - ) - ) { - val language = - paramOrDefault(this.language.paramName, AllLanguages) - val id = long(this.learningpathId.paramName) - val fallback = booleanOrDefault(this.fallback.paramName, default = false) - - readService.learningstepsForWithStatusV2( - id, - StepStatus.ACTIVE, - language, - fallback, - UserInfo.getUserOrPublic - ) match { - case Success(x) => Ok(x) - case Failure(ex) => errorHandler(ex) + + def getLearningStep: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Fetch details about the specified learningstep") + .description("Show the given learningstep for the given learningpath") + .in(pathLearningpathId / "learningsteps" / pathLearningstepId) + .in(language) + .in(fallback) + .out(jsonBody[LearningStepV2]) + .errorOut(errorOutputsFor(401, 403, 404)) + .withOptionalUser + .serverLogicPure { maybeUser => + { case (pathId, stepId, language, fallback) => + readService + .learningstepV2For(pathId, stepId, language, fallback, maybeUser.getOrElse(TokenUser.PublicUser)) + .handleErrorsOrOk + } } - }: Unit - - get( - "/:learningpath_id/learningsteps/:learningstep_id", - operation( - apiOperation[LearningStepV2]("getLearningstep") - .summary("Fetch details about the specified learningstep") - .description("Show the given learningstep for the given learningpath") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - asPathParam(learningstepId), - asQueryParam(language), - asQueryParam(fallback) - ) - .responseMessages(response403, response404, response500, response502) - .authorizations("oauth2") - ) - ) { - val language = - paramOrDefault(this.language.paramName, AllLanguages) - val pathId = long(this.learningpathId.paramName) - val stepId = long(this.learningstepId.paramName) - val fallback = booleanOrDefault(this.fallback.paramName, default = false) - - readService.learningstepV2For(pathId, stepId, language, fallback, UserInfo.getUserOrPublic) match { - case Success(step) => Ok(step) - case Failure(ex) => errorHandler(ex) + + def getLearningStepsInTrash: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Fetch deleted learningsteps for given learningpath") + .description("Show all learningsteps for the given learningpath that are marked as deleted") + .in(pathLearningpathId / "learningsteps" / "trash") + .in(language) + .in(fallback) + .out(jsonBody[LearningStepContainerSummary]) + .errorOut(errorOutputsFor(401, 403, 404)) + .requirePermission() + .serverLogicPure { user => + { case (id, language, fallback) => + readService.learningstepsForWithStatusV2(id, StepStatus.DELETED, language, fallback, user).handleErrorsOrOk + } } - }: Unit - - get( - "/:learningpath_id/learningsteps/trash/", - operation( - apiOperation[List[LearningStepContainerSummary]]("getLearningStepsInTrash") - .summary("Fetch deleted learningsteps for given learningpath") - .description("Show all learningsteps for the given learningpath that are marked as deleted") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - asQueryParam(language), - asQueryParam(fallback) - ) - .responseMessages(response403, response404, response500) - .authorizations("oauth2") - ) - ) { - requireUserId { userInfo => - val language = paramOrDefault(this.language.paramName, AllLanguages) - val id = long(this.learningpathId.paramName) - val fallback = booleanOrDefault(this.fallback.paramName, default = false) - - readService.learningstepsForWithStatusV2(id, StepStatus.DELETED, language, fallback, userInfo) match { - case Success(x) => Ok(x) - case Failure(ex) => errorHandler(ex) + + def getLearningStepStatus: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Show status information for learningstep") + .description("Shows status for the learningstep") + .in(pathLearningpathId / "learningsteps" / pathLearningstepId / "status") + .in(fallback) + .out(jsonBody[LearningStepStatus]) + .errorOut(errorOutputsFor(401, 403, 404)) + .withOptionalUser + .serverLogicPure { maybeUser => + { case (pathId, stepId, fallback) => + val user = maybeUser.getOrElse(TokenUser.PublicUser) + readService + .learningStepStatusForV2( + pathId, + stepId, + DefaultLanguage, + fallback, + user + ) + .handleErrorsOrOk } } - }: Unit - - get( - "/:learningpath_id/learningsteps/:learningstep_id/status/", - operation( - apiOperation[LearningStepStatus]("getLearningStepStatus") - .summary("Show status information for learningstep") - .description("Shows status for the learningstep") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - asPathParam(learningstepId), - asQueryParam(fallback) - ) - .responseMessages(response403, response404, response500) - .authorizations("oauth2") - ) - ) { - val pathId = long(this.learningpathId.paramName) - val stepId = long(this.learningstepId.paramName) - val fallback = booleanOrDefault(this.fallback.paramName, default = false) - - readService.learningStepStatusForV2(pathId, stepId, DefaultLanguage, fallback, UserInfo.getUserOrPublic) match { - case Success(status) => Ok(status) - case Failure(ex) => errorHandler(ex) + + def getMyLearningpaths: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Fetch all learningspaths you have created") + .description("Shows your learningpaths.") + .in("mine") + .out(jsonBody[List[LearningPathSummaryV2]]) + .errorOut(errorOutputsFor(401, 403, 404)) + .requirePermission() + .serverLogicPure { user => _ => readService.withOwnerV2(user).asRight } + + def getLicenses: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Show all valid licenses") + .description("Shows all valid licenses") + .in("licenses") + .in(licenseFilter) + .out(jsonBody[Seq[License]]) + .errorOut(errorOutputsFor(401, 403, 404)) + .serverLogicPure { license => + val licenses: Seq[LicenseDefinition] = + license match { + case None => mapping.License.getLicenses + case Some(filter) => + mapping.License.getLicenses + .filter(_.license.toString.contains(filter)) + } + licenses.map(x => License(x.license.toString, Option(x.description), x.url)).asRight } - }: Unit - - get( - "/mine/", - operation( - apiOperation[List[LearningPathSummaryV2]]("getMyLearningpaths") - .summary("Fetch all learningspaths you have created") - .description("Shows your learningpaths.") - .parameters(asHeaderParam(correlationId)) - .responseMessages(response403, response500) - .authorizations("oauth2") - ) - ) { - requireUserId(readService.withOwnerV2) - }: Unit - - get( - "/licenses/", - operation( - apiOperation[List[License]]("getLicenses") - .summary("Show all valid licenses") - .description("Shows all valid licenses") - .parameters( - asHeaderParam(correlationId), - asQueryParam(licenseFilter) - ) - .responseMessages(response403, response500) - .authorizations("oauth2") - ) - ) { - val licenses: Seq[LicenseDefinition] = - paramOrNone(this.licenseFilter.paramName) match { - case None => mapping.License.getLicenses - case Some(filter) => - mapping.License.getLicenses - .filter(_.license.toString.contains(filter)) - } - licenses.map(x => License(x.license.toString, Option(x.description), x.url)) - }: Unit - - post( - "/", - operation( - apiOperation[LearningPathV2]("addLearningpath") - .summary("Store new learningpath") - .description("Adds the given learningpath") - .parameters( - asHeaderParam(correlationId), - bodyParam[NewLearningPathV2] - ) - .responseMessages(response400, response403, response404, response500) - .authorizations("oauth2") - ) - ) { - requireUserId { userInfo => - val newLearningPath = extract[NewLearningPathV2](request.body) - updateService.addLearningPathV2(newLearningPath, userInfo) match { - case Failure(ex) => errorHandler(ex) + def addLearningpath: ServerEndpoint[Any, Eff] = endpoint.post + .summary("Store new learningpath") + .description("Adds the given learningpath") + .in(jsonBody[NewLearningPathV2]) + .out(statusCode(StatusCode.Created).and(jsonBody[LearningPathV2])) + .out(EndpointOutput.derived[DynamicHeaders]) + .errorOut(errorOutputsFor(400, 401, 403, 404)) + .requirePermission() + .serverLogicPure { user => newLearningPath => + updateService.addLearningPathV2(newLearningPath, user) match { + case Failure(ex) => returnLeftError(ex) case Success(learningPath) => logger.info(s"CREATED LearningPath with ID = ${learningPath.id}") - Created(headers = Map("Location" -> learningPath.metaUrl), body = learningPath) + val headers = DynamicHeaders.fromValue("Location", learningPath.metaUrl) + (learningPath, headers).asRight } } - }: Unit - - post( - "/:learningpath_id/copy/", - operation( - apiOperation[LearningPathV2]("copyLearningpath") - .summary("Copy given learningpath and store it as a new learningpath") - .description("Copies the given learningpath, with the option to override some fields") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - bodyParam[NewCopyLearningPathV2] - ) - .responseMessages(response400, response403, response404, response500) - .authorizations("oauth2") - ) - ) { - UserInfo.getWithUserIdOrAdmin match { - case Failure(ex) => errorHandler(ex) - case Success(userInfo) => - val newLearningPath = extract[NewCopyLearningPathV2](request.body) - val pathId = long(this.learningpathId.paramName) - updateService.newFromExistingV2(pathId, newLearningPath, userInfo) match { - case Success(learningPath) => - logger.info(s"COPIED LearningPath with ID = ${learningPath.id}") - Created(headers = Map("Location" -> learningPath.metaUrl), body = learningPath) - case Failure(ex) => errorHandler(ex) - } + + def copyLearningpath: ServerEndpoint[Any, Eff] = endpoint.post + .summary("Copy given learningpath and store it as a new learningpath") + .description("Copies the given learningpath, with the option to override some fields") + .in(pathLearningpathId / "copy") + .in(jsonBody[NewCopyLearningPathV2]) + .out(statusCode(StatusCode.Created).and(jsonBody[LearningPathV2])) + .out(EndpointOutput.derived[DynamicHeaders]) + .errorOut(errorOutputsFor(400, 401, 403, 404)) + .requirePermission() + .serverLogicPure { user => + { case (pathId, newLearningPath) => + UserInfo + .getWithUserIdOrAdmin(user) + .flatMap(userInfo => + updateService + .newFromExistingV2(pathId, newLearningPath, userInfo) + .map(learningPath => { + logger.info(s"COPIED LearningPath with ID = ${learningPath.id}") + val headers = DynamicHeaders.fromValue("Location", learningPath.metaUrl) + (learningPath, headers) + }) + ) + .handleErrorsOrOk + } } - }: Unit - - patch( - "/:learningpath_id", - operation( - apiOperation[LearningPathV2]("updateLearningPath") - .summary("Update given learningpath") - .description("Updates the given learningPath") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - bodyParam[UpdatedLearningPathV2] - ) - .responseMessages(response400, response403, response404, response500) - .authorizations("oauth2") - ) - ) { - requireUserId { userInfo => - val pathId = long(this.learningpathId.paramName) - updateService.updateLearningPathV2(pathId, extract[UpdatedLearningPathV2](request.body), userInfo) match { - case Success(lp) => Ok(lp) - case Failure(ex) => errorHandler(ex) + + def updateLearningPath: ServerEndpoint[Any, Eff] = endpoint.patch + .summary("Update given learningpath") + .description("Updates the given learningPath") + .in(pathLearningpathId) + .in(jsonBody[UpdatedLearningPathV2]) + .out(jsonBody[LearningPathV2]) + .errorOut(errorOutputsFor(400, 401, 403, 404)) + .requirePermission() + .serverLogicPure { user => + { case (pathId, newLearningPath) => + updateService + .updateLearningPathV2( + pathId, + newLearningPath, + user + ) + .handleErrorsOrOk } } - }: Unit - - post( - "/:learningpath_id/learningsteps/", - operation( - apiOperation[LearningStepV2]("addLearningStep") - .summary("Add new learningstep to learningpath") - .description("Adds the given LearningStep") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - bodyParam[NewLearningStepV2] - ) - .responseMessages(response400, response403, response404, response500, response502) - .authorizations("oauth2") - ) - ) { - requireUserId { userInfo => - val newLearningStep = extract[NewLearningStepV2](request.body) - val pathId = long(this.learningpathId.paramName) - updateService.addLearningStepV2(pathId, newLearningStep, userInfo) match { - case Failure(ex) => errorHandler(ex) - case Success(learningStep) => - logger.info(s"CREATED LearningStep with ID = ${learningStep.id} for LearningPath with ID = $pathId") - Created(headers = Map("Location" -> learningStep.metaUrl), body = learningStep) + + def addLearningStep: ServerEndpoint[Any, Eff] = endpoint.post + .summary("Add new learningstep to learningpath") + .description("Adds the given LearningStep") + .in(pathLearningpathId / "learningsteps") + .in(jsonBody[NewLearningStepV2]) + .out(statusCode(StatusCode.Created).and(jsonBody[LearningStepV2])) + .out(EndpointOutput.derived[DynamicHeaders]) + .errorOut(errorOutputsFor(400, 401, 403, 404, 502)) + .requirePermission() + .serverLogicPure { user => + { case (pathId, newLearningStep) => + updateService + .addLearningStepV2(pathId, newLearningStep, user) + .map { learningStep => + logger.info(s"CREATED LearningStep with ID = ${learningStep.id} for LearningPath with ID = $pathId") + val headers = DynamicHeaders.fromValue("Location", learningStep.metaUrl) + (learningStep, headers) + } + .handleErrorsOrOk } } - }: Unit - - patch( - "/:learningpath_id/learningsteps/:learningstep_id", - operation( - apiOperation[LearningStepV2]("updateLearningStep") - .summary("Update given learningstep") - .description("Update the given learningStep") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - asPathParam(learningstepId), - bodyParam[UpdatedLearningStepV2] - ) - .responseMessages(response400, response403, response404, response500, response502) - .authorizations("oauth2") - ) - ) { - requireUserId { userInfo => - val updatedLearningStep = extract[UpdatedLearningStepV2](request.body) - val pathId = long(this.learningpathId.paramName) - val stepId = long(this.learningstepId.paramName) - val createdLearningStep = - updateService.updateLearningStepV2(pathId, stepId, updatedLearningStep, userInfo) - - createdLearningStep match { - case Failure(ex) => errorHandler(ex) - case Success(learningStep) => - logger.info(s"UPDATED LearningStep with ID = $stepId for LearningPath with ID = $pathId") - Ok(learningStep) + + def updateLearningStep: ServerEndpoint[Any, Eff] = endpoint.patch + .summary("Update given learningstep") + .description("Update the given learningStep") + .in(pathLearningpathId / "learningsteps" / pathLearningstepId) + .in(jsonBody[UpdatedLearningStepV2]) + .out(jsonBody[LearningStepV2]) + .errorOut(errorOutputsFor(400, 401, 403, 404, 502)) + .requirePermission() + .serverLogicPure { user => + { case (pathId, stepId, updatedLearningStep) => + updateService + .updateLearningStepV2(pathId, stepId, updatedLearningStep, user) + .map(learningStep => { + logger.info(s"UPDATED LearningStep with ID = $stepId for LearningPath with ID = $pathId") + learningStep + }) + .handleErrorsOrOk } } - }: Unit - - put( - "/:learningpath_id/learningsteps/:learningstep_id/seqNo/", - operation( - apiOperation[LearningStepSeqNo]("updatetLearningstepSeqNo") - .summary("Store new sequence number for learningstep.") - .description( - "Updates the sequence number for the given learningstep. The sequence number of other learningsteps will be affected by this." - ) - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - asPathParam(learningstepId), - bodyParam[LearningStepSeqNo] - ) - .responseMessages(response403, response404, response500) - .authorizations("oauth2") + + def updatedLearningstepSeqNo: ServerEndpoint[Any, Eff] = endpoint.put + .summary("Store new sequence number for learningstep.") + .description( + "Updates the sequence number for the given learningstep. The sequence number of other learningsteps will be affected by this." ) - ) { - requireUserId { userInfo => - val newSeqNo = extract[LearningStepSeqNo](request.body) - val pathId = long(this.learningpathId.paramName) - val stepId = long(this.learningstepId.paramName) - - updateService.updateSeqNo(pathId, stepId, newSeqNo.seqNo, userInfo) match { - case Success(seqNo) => Ok(seqNo) - case Failure(ex) => errorHandler(ex) + .in(pathLearningpathId / "learningsteps" / pathLearningstepId / "seqNo") + .in(jsonBody[LearningStepSeqNo]) + .out(jsonBody[LearningStepSeqNo]) + .errorOut(errorOutputsFor(400, 401, 403, 404, 502)) + .requirePermission() + .serverLogicPure { user => + { case (pathId, stepId, newSeqNo) => + updateService.updateSeqNo(pathId, stepId, newSeqNo.seqNo, user).handleErrorsOrOk } } - }: Unit - - put( - "/:learningpath_id/learningsteps/:learningstep_id/status/", - operation( - apiOperation[LearningStepV2]("updateLearningStepStatus") - .summary("Update status of given learningstep") - .description("Updates the status of the given learningstep") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - asPathParam(learningstepId), - bodyParam[LearningStepStatus] - ) - .responseMessages(response400, response403, response404, response500) - .authorizations("oauth2") - ) - ) { - requireUserId { userInfo => - val learningStepStatus = extract[LearningStepStatus](request.body) - val stepStatus = StepStatus.valueOfOrError(learningStepStatus.status) - val pathId = long(this.learningpathId.paramName) - val stepId = long(this.learningstepId.paramName) - - val updatedStep = updateService.updateLearningStepStatusV2(pathId, stepId, stepStatus, userInfo) - - updatedStep match { - case Failure(ex) => errorHandler(ex) - case Success(learningStep) => - logger.info( - s"UPDATED LearningStep with id: $stepId for LearningPath with id: $pathId to STATUS = ${learningStep.status}" - ) - Ok(learningStep) + + def updateLearningStepStatus: ServerEndpoint[Any, Eff] = endpoint.put + .summary("Update status of given learningstep") + .description("Updates the status of the given learningstep") + .in(pathLearningpathId / "learningsteps" / pathLearningstepId / "status") + .in(jsonBody[LearningStepStatus]) + .out(jsonBody[LearningStepV2]) + .errorOut(errorOutputsFor(400, 401, 403, 404)) + .requirePermission() + .serverLogicPure { user => + { case (pathId, stepId, learningStepStatus) => + val stepStatus = StepStatus.valueOfOrError(learningStepStatus.status) + updateService + .updateLearningStepStatusV2(pathId, stepId, stepStatus, user) + .map(learningStep => { + logger.info( + s"UPDATED LearningStep with id: $stepId for LearningPath with id: $pathId to STATUS = ${learningStep.status}" + ) + learningStep + }) + .handleErrorsOrOk } } - }: Unit - - put( - "/:learningpath_id/status/", - operation( - apiOperation[LearningPathV2]("updateLearningPathStatus") - .summary("Update status of given learningpath") - .description("Updates the status of the given learningPath") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - bodyParam[UpdateLearningPathStatus] - ) - .responseMessages(response400, response403, response404, response500) - .authorizations("oauth2") - ) - ) { - requireUserId { userInfo => - val toUpdate = extract[UpdateLearningPathStatus](request.body) - val pathStatus = domain.LearningPathStatus.valueOfOrError(toUpdate.status) - val pathId = long(this.learningpathId.paramName) - updateService.updateLearningPathStatusV2( - pathId, - pathStatus, - userInfo, - DefaultLanguage, - toUpdate.message - ) match { - case Failure(ex) => errorHandler(ex) - case Success(learningPath) => - logger.info(s"UPDATED status of LearningPath with ID = ${learningPath.id}") - Ok(learningPath) + def updateLearningPathStatus: ServerEndpoint[Any, Eff] = endpoint.put + .summary("Update status of given learningpath") + .description("Updates the status of the given learningPath") + .in(pathLearningpathId / "status") + .in(jsonBody[UpdateLearningPathStatus]) + .out(jsonBody[LearningPathV2]) + .errorOut(errorOutputsFor(400, 403, 404, 500)) + .requirePermission() + .serverLogicPure { user => + { case (pathId, updateLearningPathStatus) => + val pathStatus = domain.LearningPathStatus.valueOfOrError(updateLearningPathStatus.status) + updateService + .updateLearningPathStatusV2(pathId, pathStatus, user, DefaultLanguage, updateLearningPathStatus.message) + .map(learningPath => { + logger.info(s"UPDATED status of LearningPath with ID = ${learningPath.id}") + learningPath + }) + .handleErrorsOrOk } } - }: Unit - - get( - s"/status/:${this.learningPathStatus.paramName}", - operation( - apiOperation[List[LearningPathV2]]("withStatus") - .summary("Fetch all learningpaths with specified status") - .description("Fetch all learningpaths with specified status") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningPathStatus) - ) - .responseMessages(response500, response400) - .authorizations("oauth2") - ) - ) { - val pathStatus = params(this.learningPathStatus.paramName) - readService.learningPathWithStatus(pathStatus, UserInfo.getUserOrPublic) match { - case Success(lps) => Ok(lps) - case Failure(ex) => errorHandler(ex) + + def withStatus: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Fetch all learningpaths with specified status") + .description("Fetch all learningpaths with specified status") + .in("status" / learningPathStatus) + .out(jsonBody[List[LearningPathV2]]) + .errorOut(errorOutputsFor(400, 500)) + .withOptionalUser + .serverLogicPure { maybeUser => + { case status => + val user = maybeUser.getOrElse(TokenUser.PublicUser) + readService.learningPathWithStatus(status, user).handleErrorsOrOk + } } - }: Unit - - delete( - "/:learningpath_id", - operation( - apiOperation[Unit]("deleteLearningPath") - .summary("Delete given learningpath") - .description("Deletes the given learningPath") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId) - ) - .responseMessages(response403, response404, response500) - .authorizations("oauth2") - ) - ) { - requireUserId { userInfo => - val pathId = long(this.learningpathId.paramName) + def deleteLearningpath: ServerEndpoint[Any, Eff] = endpoint.delete + .summary("Delete given learningpath") + .description("Deletes the given learningPath") + .in(pathLearningpathId) + .out(statusCode(StatusCode.NoContent).and(emptyOutput)) + .errorOut(errorOutputsFor(403, 404, 500)) + .requirePermission() + .serverLogicPure { user => pathId => updateService.updateLearningPathStatusV2( pathId, domain.LearningPathStatus.DELETED, - userInfo, + user, DefaultLanguage ) match { - case Failure(ex) => errorHandler(ex) + case Failure(ex) => returnLeftError(ex) case Success(_) => logger.info(s"MARKED LearningPath with ID: $pathId as DELETED") - NoContent() + ().asRight } } - }: Unit - - delete( - "/:learningpath_id/learningsteps/:learningstep_id", - operation( - apiOperation[Unit]("deleteLearningStep") - .summary("Delete given learningstep") - .description("Deletes the given learningStep") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - asPathParam(learningstepId) - ) - .responseMessages(response403, response404, response500) - .authorizations("oauth2") - ) - ) { - requireUserId { userInfo => - val pathId = long(this.learningpathId.paramName) - val stepId = long(this.learningstepId.paramName) - val deleted = updateService.updateLearningStepStatusV2(pathId, stepId, StepStatus.DELETED, userInfo) - deleted match { - case Failure(ex) => errorHandler(ex) - case Success(_) => - logger.info(s"MARKED LearningStep with id: $stepId for LearningPath with id: $pathId as DELETED.") - NoContent() + + def deleteLearningStep: ServerEndpoint[Any, Eff] = endpoint.delete + .summary("Delete given learningstep") + .description("Deletes the given learningStep") + .in(pathLearningpathId / "learningsteps" / pathLearningstepId) + .out(statusCode(StatusCode.NoContent).and(emptyOutput)) + .errorOut(errorOutputsFor(403, 404, 500)) + .requirePermission() + .serverLogicPure { user => + { case (pathId, stepId) => + updateService.updateLearningStepStatusV2(pathId, stepId, StepStatus.DELETED, user) match { + case Failure(ex) => returnLeftError(ex) + case Success(_) => + logger.info(s"MARKED LearningStep with id: $stepId for LearningPath with id: $pathId as DELETED") + ().asRight + } } } - }: Unit - - get( - "/tags/", - operation( - apiOperation[LearningPathTagsSummary]("getTags") - .summary("Fetch all previously used tags in learningpaths") - .description("Retrieves a list of all previously used tags in learningpaths") - .parameters( - asHeaderParam(correlationId), - asQueryParam(language), - asQueryParam(fallback) - ) - .responseMessages(response500) - .authorizations("oauth2") - ) - ) { - val language = - paramOrDefault(this.language.paramName, AllLanguages) - val allTags = readService.tags - val fallback = booleanOrDefault(this.fallback.paramName, default = false) - - converterService.asApiLearningPathTagsSummary(allTags, language, fallback) match { - case Some(s) => s - case None => - NotFound(Error(ErrorHelpers.NOT_FOUND, s"Tags with language '$language' not found")) + + def getTags: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Fetch all previously used tags in learningpaths") + .description("Retrieves a list of all previously used tags in learningpaths") + .in("tags") + .in(language) + .in(fallback) + .out(jsonBody[LearningPathTagsSummary]) + .errorOut(errorOutputsFor(500)) + .serverLogicPure { case (language, fallback) => + val allTags = readService.tags + converterService.asApiLearningPathTagsSummary(allTags, language, fallback) match { + case Some(s) => s.asRight + case None => notFoundWithMsg(s"Tags with language '$language' not found").asLeft + } } - }: Unit - - get( - "/contributors/", - operation( - apiOperation[List[Author]]("getContributors") - .summary("Fetch all previously used contributors in learningpaths") - .description("Retrieves a list of all previously used contributors in learningpaths") - .parameters(asHeaderParam(correlationId)) - .responseMessages(response500) - .authorizations("oauth2") - ) - ) { - readService.contributors - }: Unit - - post( - "/:learningpath_id/update-taxonomy/", - operation( - apiOperation[LearningPathV2]("updateLearningPathTaxonomy") - .summary("Update taxonomy for specified learningpath") - .description("Update taxonomy for specified learningpath") - .parameters( - asHeaderParam(correlationId), - asPathParam(learningpathId), - asQueryParam(language), - asQueryParam(fallback), - asQueryParam(createResourceIfMissing) - ) - .responseMessages(response403, response404, response500) - .authorizations("oauth2") - ) - ) { - requireUserId { userInfo => - val pathId = long(this.learningpathId.paramName) - val language = paramOrDefault(this.language.paramName, AllLanguages) - val fallback = booleanOrDefault(this.fallback.paramName, default = false) - val createResourceIfMissing = booleanOrDefault(this.createResourceIfMissing.paramName, default = false) - - updateService.updateTaxonomyForLearningPath( - pathId, - createResourceIfMissing, - language, - fallback, - userInfo - ) match { - case Success(lp) => Ok(lp) - case Failure(ex) => errorHandler(ex) + + def getContributors: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Fetch all previously used contributors in learningpaths") + .description("Retrieves a list of all previously used contributors in learningpaths") + .in("contributors") + .out(jsonBody[List[Author]]) + .errorOut(errorOutputsFor()) + .serverLogicPure { _ => + readService.contributors.asRight + } + + def updateLearningPathTaxonomy: ServerEndpoint[Any, Eff] = endpoint.post + .summary("Update taxonomy for specified learningpath") + .description("Update taxonomy for specified learningpath") + .in(pathLearningpathId / "update-taxonomy") + .in(language) + .in(fallback) + .in(createResourceIfMissing) + .out(jsonBody[LearningPathV2]) + .errorOut(errorOutputsFor(403, 404, 500)) + .requirePermission() + .serverLogicPure { userInfo => + { case (pathId, language, fallback, createResourceIfMissing) => + updateService + .updateTaxonomyForLearningPath( + pathId, + createResourceIfMissing, + language, + fallback, + userInfo + ) + .handleErrorsOrOk } } - }: Unit - - get( - "/contains-article/:article_id", - operation( - apiOperation[List[LearningPathSummaryV2]]("fetchLearningPathContainingArticle") - .summary("Fetch learningpaths containing specified article") - .description("Fetch learningpaths containing specified article") - .parameters( - asPathParam(articleId) - ) - ) - ) { - val articleId = long(this.articleId.paramName) - val nodes = taxonomyApiClient.queryNodes(articleId).getOrElse(List.empty).flatMap(_.paths) - val plainPaths = List( - s"/article-iframe/*/$articleId", - s"/article-iframe/*/$articleId/", - s"/article-iframe/*/$articleId/\\?*", - s"/article-iframe/*/$articleId\\?*", - s"/article/$articleId" - ) - val paths = nodes ++ plainPaths - searchService.containsPath(paths) match { - case Success(result) => result.results - case Failure(ex) => errorHandler(ex) + def fetchLearningPathContainingArticle: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Fetch learningpaths containing specified article") + .description("Fetch learningpaths containing specified article") + .in("contains-article" / pathArticleId) + .out(jsonBody[Seq[LearningPathSummaryV2]]) + .errorOut(errorOutputsFor(400, 500)) + .serverLogicPure { articleId => + val nodes = taxonomyApiClient.queryNodes(articleId).getOrElse(List.empty).flatMap(_.paths) + val plainPaths = List( + s"/article-iframe/*/$articleId", + s"/article-iframe/*/$articleId/", + s"/article-iframe/*/$articleId/\\?*", + s"/article-iframe/*/$articleId\\?*", + s"/article/$articleId" + ) + val paths = nodes ++ plainPaths + + searchService.containsPath(paths) match { + case Success(result) => result.results.asRight + case Failure(ex) => returnLeftError(ex) + } } - }: Unit } } diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/NdlaController.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/NdlaController.scala deleted file mode 100644 index 7de0cb0d2c..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/NdlaController.scala +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.controller - -import no.ndla.common.errors.{AccessDeniedException, NotFoundException, ValidationException} -import no.ndla.learningpathapi.integration.DataSource -import no.ndla.learningpathapi.model.api.{Error, ErrorHelpers, ValidationError} -import no.ndla.learningpathapi.model.domain._ -import no.ndla.learningpathapi.service.ConverterService -import no.ndla.myndla.model.domain.InvalidStatusException -import no.ndla.network.model.HttpRequestException -import no.ndla.network.scalatra.NdlaSwaggerSupport -import no.ndla.search.{IndexNotFoundException, NdlaSearchException} -import org.json4s.{DefaultFormats, Formats} -import org.postgresql.util.PSQLException -import org.scalatra._ - -import scala.util.{Failure, Success, Try} - -trait NdlaController { - this: DataSource with ErrorHelpers with ConverterService with NdlaSwaggerSupport => - - abstract class NdlaController extends NdlaSwaggerSupport { - protected implicit override val jsonFormats: Formats = DefaultFormats - - before() { - contentType = formats("json") - } - - // This lets us return Try[T] and handle errors automatically, otherwise return 200 OK :^) - val tryRenderer: RenderPipeline = { - case Success(value) => value - case Failure(ex) => - Try(errorHandler(ex)) match { - case Failure(ex: HaltException) => renderHaltException(ex) - case Failure(ex) => errorHandler(ex) - case Success(result) => result - } - } - - override def renderPipeline: RenderPipeline = tryRenderer.orElse(super.renderPipeline) - - import ErrorHelpers._ - - override def ndlaErrorHandler: NdlaErrorHandler = { - case v: ValidationException => - BadRequest(body = ValidationError(VALIDATION, VALIDATION_DESCRIPTION, messages = v.errors)) - case a: AccessDeniedException => - Forbidden(body = Error(ACCESS_DENIED, a.getMessage)) - case _: OptimisticLockException => - Conflict(body = Error(RESOURCE_OUTDATED, RESOURCE_OUTDATED_DESCRIPTION)) - case nfe: NotFoundException => - NotFound(Error(NOT_FOUND, nfe.getMessage)) - case hre: HttpRequestException => - BadGateway(body = Error(REMOTE_ERROR, hre.getMessage)) - case i: ImportException => - UnprocessableEntity(body = Error(IMPORT_FAILED, i.getMessage)) - case rw: ResultWindowTooLargeException => - UnprocessableEntity(body = Error(WINDOW_TOO_LARGE, rw.getMessage)) - case _: IndexNotFoundException => - InternalServerError(body = IndexMissingError) - case i: ElasticIndexingException => - InternalServerError(body = Error(GENERIC, i.getMessage)) - case _: PSQLException => - DataSource.connectToDatabase() - InternalServerError(DatabaseUnavailableError) - case mse: InvalidStatusException => - BadRequest(Error(MISSING_STATUS, mse.getMessage)) - case NdlaSearchException(_, Some(rf), _) - if rf.error.rootCause - .exists(x => x.`type` == "search_context_missing_exception" || x.reason == "Cannot parse scroll id") => - BadRequest(body = InvalidSearchContext) - case t: Throwable => - logger.error(t.getMessage, t) - InternalServerError(body = Error(GENERIC, GENERIC_DESCRIPTION)) - } - } -} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/StatsController.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/StatsController.scala index 17f8f9c719..c7e332edbc 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/StatsController.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/StatsController.scala @@ -7,41 +7,31 @@ package no.ndla.learningpathapi.controller -import no.ndla.learningpathapi.Props -import no.ndla.learningpathapi.model.api.Error +import no.ndla.learningpathapi.{Eff, Props} +import no.ndla.learningpathapi.model.api.ErrorHelpers import no.ndla.learningpathapi.service.ReadService -import no.ndla.myndla.model.api.Stats -import no.ndla.network.scalatra.NdlaSwaggerSupport -import org.json4s.{DefaultFormats, Formats} -import org.scalatra.swagger.{ResponseMessage, Swagger} -import org.scalatra.MovedPermanently +import no.ndla.network.tapir.Service +import sttp.model.StatusCode +import sttp.tapir.EndpointInput +import sttp.tapir._ +import sttp.tapir.server.ServerEndpoint trait StatsController { - this: ReadService with NdlaController with Props with NdlaSwaggerSupport => + this: ReadService with Props with ErrorHelpers => val statsController: StatsController - class StatsController(implicit val swagger: Swagger) extends NdlaController with NdlaSwaggerSupport { - protected implicit override val jsonFormats: Formats = DefaultFormats - - protected val applicationDescription = "API for getting stats for my-ndla usage." - - // Additional models used in error responses - registerModel[Error]() - - private val response301: ResponseMessage = ResponseMessage(301, "Moved permanently", Some("Error")) - - get( - "/", - operation( - apiOperation[Stats]("getStats") - .summary("Get stats for my-ndla usage.") - .description("Get stats for my-ndla usage.") - .responseMessages(response301) - .deprecated(true) - ) - ) { - MovedPermanently("/myndla-api/v1/stats") - }: Unit + class StatsController extends Service[Eff] { + override val serviceName: String = "stats" + override val prefix: EndpointInput[Unit] = "learningpath-api" / "v1" / serviceName + override val endpoints: List[ServerEndpoint[Any, Eff]] = List( + getStats + ) + + def getStats: ServerEndpoint[Any, Eff] = endpoint.get + .summary("Get stats for my-ndla usage.") + .description("Get stats for my-ndla usage.") + .deprecated() + .errorOut(statusCode(StatusCode.MovedPermanently).and(header("Location", "/myndla-api/v1/stats"))) + .serverLogicPure(_ => Left(())) } - } diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/SwaggerDocControllerConfig.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/SwaggerDocControllerConfig.scala new file mode 100644 index 0000000000..4de94e5c4f --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/SwaggerDocControllerConfig.scala @@ -0,0 +1,28 @@ +/* + * Part of NDLA learningpath-api + * Copyright (C) 2024 NDLA + * + * See LICENSE + */ + +package no.ndla.learningpathapi.controller + +import no.ndla.learningpathapi.Props +import no.ndla.network.tapir.auth.Permission +import no.ndla.network.tapir.{SwaggerControllerConfig, SwaggerInfo} +import sttp.tapir._ + +trait SwaggerDocControllerConfig extends SwaggerControllerConfig { + this: Props => + + object SwaggerDocControllerConfig { + private val scopes = Permission.toSwaggerMap(Permission.thatStartsWith("learningpath")) + + val swaggerInfo: SwaggerInfo = SwaggerInfo( + mountPoint = "learningpath-api" / "api-docs", + description = "Services for accessing learningpaths", + authUrl = props.Auth0LoginEndpoint, + scopes = scopes + ) + } +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/UserController.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/UserController.scala deleted file mode 100644 index 7140a7bf59..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/controller/UserController.scala +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2022 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.controller - -import no.ndla.common.model.NDLADate -import no.ndla.learningpathapi.model.api.{Error, ValidationError} -import no.ndla.learningpathapi.service.{ConverterService, ReadService, UpdateService} -import no.ndla.myndla.model.api.{ExportedUserData, MyNDLAUser, UpdatedMyNDLAUser} -import no.ndla.myndla.service.{FolderReadService, FolderWriteService, UserService} -import no.ndla.network.tapir.auth.Permission.LEARNINGPATH_API_ADMIN -import org.json4s.ext.JavaTimeSerializers -import org.json4s.{DefaultFormats, Formats} -import org.scalatra.NoContent -import org.scalatra.swagger._ - -import javax.servlet.http.HttpServletRequest - -trait UserController { - this: ReadService - with UpdateService - with ConverterService - with NdlaController - with UserService - with FolderWriteService - with FolderReadService => - val userController: UserController - - class UserController(implicit val swagger: Swagger) extends NdlaController { - - protected implicit override val jsonFormats: Formats = - DefaultFormats ++ - JavaTimeSerializers.all + - NDLADate.Json4sSerializer - - protected val applicationDescription = "API for accessing My NDLA from ndla.no." - - // Additional models used in error responses - registerModel[ValidationError]() - registerModel[Error]() - - val response403: ResponseMessage = ResponseMessage(403, "Access not granted", Some("Error")) - val response404: ResponseMessage = ResponseMessage(404, "Not Found", Some("Error")) - val response500: ResponseMessage = ResponseMessage(500, "Unknown error", Some("Error")) - - private val feideToken = Param[Option[String]]("FeideAuthorization", "Header containing FEIDE access token.") - private val feideId = Param[Option[String]]("feide-id", "FeideID of user") - - private def requestFeideToken(implicit request: HttpServletRequest): Option[String] = { - request.header(this.feideToken.paramName).map(_.replaceFirst("Bearer ", "")) - } - - get( - "/", - operation( - apiOperation[MyNDLAUser]("GetMyNDLAUser") - .summary("Get user data") - .description("Get user data") - .parameters( - asHeaderParam(feideToken) - ) - .deprecated(true) - .responseMessages(response403, response500) - .authorizations("oauth2") - ) - ) { - userService.getMyNDLAUserData(requestFeideToken) - }: Unit - - patch( - "/", - operation( - apiOperation[MyNDLAUser]("UpdateMyNDLAUser") - .summary("Update user data") - .description("Update user data") - .parameters( - asHeaderParam(feideToken), - bodyParam[UpdatedMyNDLAUser] - ) - .deprecated(true) - .responseMessages(response403, response404, response500) - .authorizations("oauth2") - ) - ) { - val updatedUserData = extract[UpdatedMyNDLAUser](request.body) - userService.updateMyNDLAUserData(updatedUserData, requestFeideToken) - }: Unit - - patch( - "/update-other-user/?", - operation( - apiOperation[MyNDLAUser]("AdminUpdateMyNDLAUser") - .summary("Update some one elses user data") - .description("Update some one elses user data") - .parameters( - asQueryParam(feideId), - bodyParam[UpdatedMyNDLAUser] - ) - .deprecated(true) - .responseMessages(response403, response404, response500) - .authorizations("oauth2") - ) - ) { - requirePermissionOrAccessDeniedWithUser(LEARNINGPATH_API_ADMIN) { user => - val updatedUserData = extract[UpdatedMyNDLAUser](request.body) - val feideId = paramOrNone(this.feideId.paramName) - userService.adminUpdateMyNDLAUserData(updatedUserData, feideId, Some(user), None) - } - }: Unit - - delete( - "/delete-personal-data/?", - operation( - apiOperation[Unit]("DeleteAllUserData") - .summary("Delete all data connected to this user") - .description("Delete all data connected to this user") - .parameters( - asHeaderParam(feideToken) - ) - .deprecated(true) - .responseMessages(response403, response500) - .authorizations("oauth2") - ) - ) { - folderWriteService.deleteAllUserData(requestFeideToken).map(_ => NoContent()) - }: Unit - - get( - "/export", - operation( - apiOperation[ExportedUserData]("exportUserData") - .summary("Export all stored user-related data as a json structure") - .description("Export all stored user-related data as a json structure") - .parameters(asHeaderParam(feideToken)) - .deprecated(true) - ) - ) { - folderReadService.exportUserData(requestFeideToken) - }: Unit - - post( - "/import", - operation( - apiOperation[ExportedUserData]("importUserData") - .summary("Import all stored user-related data from a exported json structure") - .description("Import all stored user-related data from a exported json structure") - .parameters( - asHeaderParam(feideToken), - bodyParam[ExportedUserData] - ) - .deprecated(true) - ) - ) { - val importBody = tryExtract[ExportedUserData](request.body) - importBody.flatMap(importBody => folderWriteService.importUserData(importBody, requestFeideToken)) - }: Unit - } -} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/db/migrationwithdependencies/V14__ConvertLanguageUnknown.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/db/migrationwithdependencies/V14__ConvertLanguageUnknown.scala index 5eb5d03a76..28f8911b4b 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/db/migrationwithdependencies/V14__ConvertLanguageUnknown.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/db/migrationwithdependencies/V14__ConvertLanguageUnknown.scala @@ -9,19 +9,16 @@ package no.ndla.learningpathapi.db.migrationwithdependencies import no.ndla.common.model.domain.learningpath.EmbedType import no.ndla.learningpathapi.{LearningpathApiProperties, Props} -import no.ndla.learningpathapi.model.domain.DBLearningStep +import no.ndla.learningpathapi.model.domain.LearningStep import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} import org.json4s.native.JsonMethods.{compact, parse, render} import org.json4s.{Extraction, Formats} import org.postgresql.util.PGobject import scalikejdbc._ -class V14__ConvertLanguageUnknown(properties: LearningpathApiProperties) - extends BaseJavaMigration - with Props - with DBLearningStep { +class V14__ConvertLanguageUnknown(properties: LearningpathApiProperties) extends BaseJavaMigration with Props { override val props = properties - implicit val formats: Formats = DBLearningStep.jsonEncoder + implicit val formats: Formats = LearningStep.jsonEncoder override def migrate(context: Context): Unit = DB(context.getConnection) .autoClose(false) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/db/migrationwithdependencies/V15__MergeDuplicateLanguageFields.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/db/migrationwithdependencies/V15__MergeDuplicateLanguageFields.scala index ec93427991..7fea8e3f0f 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/db/migrationwithdependencies/V15__MergeDuplicateLanguageFields.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/db/migrationwithdependencies/V15__MergeDuplicateLanguageFields.scala @@ -8,7 +8,7 @@ package no.ndla.learningpathapi.db.migrationwithdependencies import no.ndla.learningpathapi.{LearningpathApiProperties, Props} -import no.ndla.learningpathapi.model.domain.DBLearningStep +import no.ndla.learningpathapi.model.domain.LearningStep import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} import org.json4s.Formats import org.json4s.JsonAST._ @@ -16,12 +16,9 @@ import org.json4s.native.JsonMethods.{compact, parse, render} import org.postgresql.util.PGobject import scalikejdbc.{DB, DBSession, _} -class V15__MergeDuplicateLanguageFields(properties: LearningpathApiProperties) - extends BaseJavaMigration - with Props - with DBLearningStep { +class V15__MergeDuplicateLanguageFields(properties: LearningpathApiProperties) extends BaseJavaMigration with Props { override val props = properties - implicit val formats: Formats = DBLearningStep.jsonEncoder + implicit val formats: Formats = LearningStep.jsonEncoder override def migrate(context: Context): Unit = DB(context.getConnection) .autoClose(false) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Copyright.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Copyright.scala index 03c8b44f9e..14591c58ca 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Copyright.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Copyright.scala @@ -8,14 +8,18 @@ package no.ndla.learningpathapi.model.api +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.common.model.api.{Author, License} -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Description of copyright information") +@description("Description of copyright information") case class Copyright( - @(ApiModelProperty @field)(description = "Describes the license of the learningpath") license: License, - @(ApiModelProperty @field)(description = "List of authors") contributors: Seq[Author] + @description("Describes the license of the learningpath") license: License, + @description("List of authors") contributors: Seq[Author] ) + +object Copyright { + implicit val encoder: Encoder[Copyright] = deriveEncoder + implicit val decoder: Decoder[Copyright] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/CoverPhoto.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/CoverPhoto.scala index d324289da2..c9734515a9 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/CoverPhoto.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/CoverPhoto.scala @@ -8,4 +8,12 @@ package no.ndla.learningpathapi.model.api +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} + case class CoverPhoto(url: String, metaUrl: String) + +object CoverPhoto { + implicit val encoder: Encoder[CoverPhoto] = deriveEncoder + implicit val decoder: Decoder[CoverPhoto] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Description.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Description.scala index 9ba0139ad5..fcc9209f7d 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Description.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Description.scala @@ -8,19 +8,21 @@ package no.ndla.learningpathapi.model.api +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.language.model.LanguageField -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "The description of the learningpath") +@description("The description of the learningpath") case class Description( - @(ApiModelProperty @field)(description = "The learningpath description. Basic HTML allowed") description: String, - @(ApiModelProperty @field)( - description = "ISO 639-1 code that represents the language used in description" - ) language: String + @description("The learningpath description. Basic HTML allowed") description: String, + @description("ISO 639-1 code that represents the language used in description") language: String ) extends LanguageField[String] { override def value: String = description override def isEmpty: Boolean = description.isEmpty } + +object Description { + implicit val encoder: Encoder[Description] = deriveEncoder + implicit val decoder: Decoder[Description] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/EmbedUrl.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/EmbedUrl.scala deleted file mode 100644 index 5d02106bee..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/EmbedUrl.scala +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field - -// format: off -@ApiModel(description = "Representation of an embeddable url") -case class EmbedUrlV2( - @(ApiModelProperty @field)(description = "The url") url: String, - @(ApiModelProperty @field)(description = "Type of embed content", allowableValues = "oembed,iframe,lti") embedType: String -) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/EmbedUrlV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/EmbedUrlV2.scala new file mode 100644 index 0000000000..274cccdfd1 --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/EmbedUrlV2.scala @@ -0,0 +1,24 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description + +@description("Representation of an embeddable url") +case class EmbedUrlV2( + @description("The url") url: String, + @description("Type of embed content") embedType: String +) + +object EmbedUrlV2 { + implicit val encoder: Encoder[EmbedUrlV2] = deriveEncoder + implicit val decoder: Decoder[EmbedUrlV2] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Error.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Error.scala deleted file mode 100644 index 7b7c0f6040..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Error.scala +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import no.ndla.learningpathapi.Props -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty -import java.time.LocalDateTime -import scala.annotation.meta.field - -@ApiModel(description = "Information about an error") -case class Error( - @(ApiModelProperty @field)(description = "Code stating the type of error") code: String, - @(ApiModelProperty @field)(description = "Description of the error") description: String, - @(ApiModelProperty @field)(description = "When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() -) - -trait ErrorHelpers { - this: Props => - object ErrorHelpers { - val GENERIC = "GENERIC" - val NOT_FOUND = "NOT_FOUND" - val INDEX_MISSING = "INDEX_MISSING" - val HEADER_MISSING = "HEADER_MISSING" - val VALIDATION = "VALIDATION" - val ACCESS_DENIED = "ACCESS_DENIED" - val REMOTE_ERROR = "REMOTE_ERROR" - val RESOURCE_OUTDATED = "RESOURCE_OUTDATED" - val WINDOW_TOO_LARGE = "RESULT WINDOW TOO LARGE" - val IMPORT_FAILED = "IMPORT_FAILED" - val DATABASE_UNAVAILABLE = "DATABASE_UNAVAILABLE" - val MISSING_STATUS = "INVALID_STATUS" - val INVALID_SEARCH_CONTEXT = "INVALID_SEARCH_CONTEXT" - val DELETE_FAVORITE = "DELETE_FAVORITE" - - val GENERIC_DESCRIPTION = - s"Ooops. Something we didn't anticipate occured. We have logged the error, and will look into it. But feel free to contact ${props.ContactEmail} if the error persists." - val VALIDATION_DESCRIPTION = "Validation Error" - - val RESOURCE_OUTDATED_DESCRIPTION = - "The resource is outdated. Please try fetching before submitting again." - - val INDEX_MISSING_DESCRIPTION = - s"Ooops. Our search index is not available at the moment, but we are trying to recreate it. Please try again in a few minutes. Feel free to contact ${props.ContactEmail} if the error persists." - - val WindowTooLargeError = Error( - WINDOW_TOO_LARGE, - s"The result window is too large. Fetching pages above ${props.ElasticSearchIndexMaxResultWindow} results requires scrolling, see query-parameter 'search-context'." - ) - val IndexMissingError = Error(INDEX_MISSING, INDEX_MISSING_DESCRIPTION) - val DatabaseUnavailableError = - Error(DATABASE_UNAVAILABLE, s"Database seems to be unavailable, retrying connection.") - val MISSING_STATUS_ERROR = "Parameter was not a valid status." - - val InvalidSearchContext = Error( - INVALID_SEARCH_CONTEXT, - "The search-context specified was not expected. Please create one by searching from page 1." - ) - } -} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/ErrorHelpers.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/ErrorHelpers.scala new file mode 100644 index 0000000000..bdac2d7b0b --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/ErrorHelpers.scala @@ -0,0 +1,63 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import no.ndla.common.Clock +import no.ndla.common.errors.{AccessDeniedException, NotFoundException, ValidationException} +import no.ndla.learningpathapi.Props +import no.ndla.learningpathapi.integration.DataSource +import no.ndla.learningpathapi.model.domain.{ElasticIndexingException, ImportException, OptimisticLockException} +import no.ndla.myndla.model.domain.InvalidStatusException +import no.ndla.network.model.HttpRequestException +import no.ndla.network.tapir.{AllErrors, TapirErrorHelpers} +import no.ndla.search.{IndexNotFoundException, NdlaSearchException} +import org.postgresql.util.PSQLException + +trait ErrorHelpers extends TapirErrorHelpers { + this: Props with Clock with DataSource => + + import ErrorHelpers._ + import LearningpathHelpers._ + override def handleErrors: PartialFunction[Throwable, AllErrors] = { + case v: ValidationException => + validationError(v) + case a: AccessDeniedException => + forbiddenMsg(a.getMessage) + case _: OptimisticLockException => + errorBody(RESOURCE_OUTDATED, RESOURCE_OUTDATED_DESCRIPTION, 409) + case nfe: NotFoundException => + notFoundWithMsg(nfe.getMessage) + case hre: HttpRequestException => + errorBody(REMOTE_ERROR, hre.getMessage, 502) + case i: ImportException => + errorBody(IMPORT_FAILED, i.getMessage, 422) + case rw: ResultWindowTooLargeException => + errorBody(WINDOW_TOO_LARGE, rw.getMessage, 413) + case _: IndexNotFoundException => + errorBody(INDEX_MISSING, INDEX_MISSING_DESCRIPTION, 500) + case i: ElasticIndexingException => + errorBody(GENERIC, i.getMessage, 500) + case _: PSQLException => + DataSource.connectToDatabase() + errorBody(DATABASE_UNAVAILABLE, DATABASE_UNAVAILABLE_DESCRIPTION, 500) + case mse: InvalidStatusException => + errorBody(MISSING_STATUS, mse.getMessage, 400) + case NdlaSearchException(_, Some(rf), _) + if rf.error.rootCause + .exists(x => x.`type` == "search_context_missing_exception" || x.reason == "Cannot parse scroll id") => + errorBody(INVALID_SEARCH_CONTEXT, INVALID_SEARCH_CONTEXT_DESCRIPTION, 400) + } + + object LearningpathHelpers { + val WINDOW_TOO_LARGE_DESCRIPTION = + s"The result window is too large. Fetching pages above ${props.ElasticSearchIndexMaxResultWindow} results requires scrolling, see query-parameter 'search-context'." + case class ResultWindowTooLargeException(message: String = LearningpathHelpers.WINDOW_TOO_LARGE_DESCRIPTION) + extends RuntimeException(message) + } +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Introduction.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Introduction.scala index 174cf25015..50e509036f 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Introduction.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Introduction.scala @@ -8,21 +8,21 @@ package no.ndla.learningpathapi.model.api +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.language.model.LanguageField -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "The introduction of the learningpath") +@description("The introduction of the learningpath") case class Introduction( - @(ApiModelProperty @field)( - description = "The introduction to the learningpath. Basic HTML allowed" - ) introduction: String, - @(ApiModelProperty @field)( - description = "ISO 639-1 code that represents the language used in introduction" - ) language: String + @description("The introduction to the learningpath. Basic HTML allowed") introduction: String, + @description("ISO 639-1 code that represents the language used in introduction") language: String ) extends LanguageField[String] { override def value: String = introduction override def isEmpty: Boolean = introduction.isEmpty } + +object Introduction { + implicit val encoder: Encoder[Introduction] = deriveEncoder + implicit val decoder: Decoder[Introduction] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPath.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPath.scala deleted file mode 100644 index c105c0230a..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPath.scala +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import no.ndla.common.model.NDLADate -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field - -// format: off -@ApiModel(description = "Meta information for a learningpath") -case class LearningPathV2( - @(ApiModelProperty @field)(description = "The unique id of the learningpath") id: Long, - @(ApiModelProperty @field)(description = "The revision number for this learningpath") revision: Int, - @(ApiModelProperty @field)(description = "The id this learningpath is based on, if any") isBasedOn: Option[Long], - @(ApiModelProperty @field)(description = "The title of the learningpath") title: Title, - @(ApiModelProperty @field)(description = "The description of the learningpath") description: Description, - @(ApiModelProperty @field)(description = "The full url to where the complete metainformation about the learningpath can be found") metaUrl: String, - @(ApiModelProperty @field)(description = "The learningsteps-summaries for this learningpath") learningsteps: Seq[LearningStepV2], - @(ApiModelProperty @field)(description = "The full url to where the learningsteps can be found") learningstepUrl: String, - @(ApiModelProperty @field)(description = "Information about where the cover photo can be found") coverPhoto: Option[CoverPhoto], - @(ApiModelProperty @field)(description = "The duration of the learningpath in minutes") duration: Option[Int], - @(ApiModelProperty @field)(description = "The publishing status of the learningpath", allowableValues = "PUBLISHED,PRIVATE,UNLISTED,SUBMITTED") status: String, - @(ApiModelProperty @field)(description = "Verification status", allowableValues = "CREATED_BY_NDLA,VERIFIED_BY_NDLA,EXTERNAL") verificationStatus: String, - @(ApiModelProperty @field)(description = "The date when this learningpath was last updated.") lastUpdated: NDLADate, - @(ApiModelProperty @field)(description = "Searchable tags for the learningpath") tags: LearningPathTags, - @(ApiModelProperty @field)(description = "Describes the copyright information for the learningpath") copyright: Copyright, - @(ApiModelProperty @field)(description = "True if authenticated user may edit this learningpath") canEdit: Boolean, - @(ApiModelProperty @field)(description = "The supported languages for this learningpath") supportedLanguages: Seq[String], - @(ApiModelProperty @field)(description = "Visible if administrator or owner of LearningPath") ownerId: Option[String], - @(ApiModelProperty @field)(description = "Message set by administrator. Visible if administrator or owner of LearningPath") message: Option[Message] -) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathDomainDump.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathDomainDump.scala index a663aba6e8..8885de737e 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathDomainDump.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathDomainDump.scala @@ -7,23 +7,33 @@ package no.ndla.learningpathapi.model.api +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.learningpathapi.model.domain.{LearningPath, LearningStep} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about learningpaths") +@description("Information about learningpaths") case class LearningPathDomainDump( - @(ApiModelProperty @field)(description = "The total number of learningpaths in the database") totalCount: Long, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The search results") results: Seq[LearningPath] + @description("The total number of learningpaths in the database") totalCount: Long, + @description("For which page results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The search results") results: Seq[LearningPath] ) -@ApiModel(description = "Information about learningsteps") +object LearningPathDomainDump { + implicit val encoder: Encoder[LearningPathDomainDump] = deriveEncoder + implicit val decoder: Decoder[LearningPathDomainDump] = deriveDecoder +} + +@description("Information about learningsteps") case class LearningStepDomainDump( - @(ApiModelProperty @field)(description = "The total number of learningsteps in the database") totalCount: Long, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The search results") results: Seq[LearningStep] + @description("The total number of learningsteps in the database") totalCount: Long, + @description("For which page results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The search results") results: Seq[LearningStep] ) + +object LearningStepDomainDump { + implicit val encoder: Encoder[LearningStepDomainDump] = deriveEncoder + implicit val decoder: Decoder[LearningStepDomainDump] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathStatus.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathStatus.scala index c4da5d8b03..8732a3c7eb 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathStatus.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathStatus.scala @@ -8,15 +8,16 @@ package no.ndla.learningpathapi.model.api -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Status information about a learningpath") +@description("Status information about a learningpath") case class LearningPathStatus( - @(ApiModelProperty @field)( - description = "The publishing status of the learningpath", - allowableValues = "PUBLISHED,PRIVATE,DELETED,UNLISTED,SUBMITTED" - ) status: String + @description("The publishing status of the learningpath") status: String ) + +object LearningPathStatus { + implicit val encoder: Encoder[LearningPathStatus] = deriveEncoder + implicit val decoder: Decoder[LearningPathStatus] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathSummary.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathSummary.scala deleted file mode 100644 index a27b1da732..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathSummary.scala +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import no.ndla.common.model.NDLADate -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field - -// format: off -@ApiModel(description = "Summary of meta information for a learningpath") -case class LearningPathSummaryV2( - @(ApiModelProperty @field)(description = "The unique id of the learningpath") id: Long, - @(ApiModelProperty @field)(description = "The revision number for this learningpath") revision: Option[Int], - @(ApiModelProperty @field)(description = "The titles of the learningpath") title: Title, - @(ApiModelProperty @field)(description = "The descriptions of the learningpath") description: Description, - @(ApiModelProperty @field)(description = "The introductions of the learningpath") introduction: Introduction, - @(ApiModelProperty @field)(description = "The full url to where the complete metainformation about the learningpath can be found") metaUrl: String, - @(ApiModelProperty @field)(description = "Url to where a cover photo can be found") coverPhotoUrl: Option[String], - @(ApiModelProperty @field)(description = "The duration of the learningpath in minutes") duration: Option[Int], - @(ApiModelProperty @field)(description = "The publishing status of the learningpath.", allowableValues = "PUBLISHED,PRIVATE,UNLISTED,SUBMITTED") status: String, - @(ApiModelProperty @field)(description = "The date when this learningpath was last updated.") lastUpdated: NDLADate, - @(ApiModelProperty @field)(description = "Searchable tags for the learningpath") tags: LearningPathTags, - @(ApiModelProperty @field)(description = "The contributors of this learningpath") copyright: Copyright, - @(ApiModelProperty @field)(description = "A list of available languages for this audio") supportedLanguages: Seq[String], - @(ApiModelProperty @field)(description = "The id this learningpath is based on, if any") isBasedOn: Option[Long], - @(ApiModelProperty @field)(description = "Message that admins can place on a LearningPath for notifying a owner of issues with the LearningPath") message: Option[String] -) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathSummaryV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathSummaryV2.scala new file mode 100644 index 0000000000..7c468f8556 --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathSummaryV2.scala @@ -0,0 +1,39 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import no.ndla.common.model.NDLADate +import sttp.tapir.Schema.annotations.description + +// format: off +@description("Summary of meta information for a learningpath") +case class LearningPathSummaryV2( + @description("The unique id of the learningpath") id: Long, + @description("The revision number for this learningpath") revision: Option[Int], + @description("The titles of the learningpath") title: Title, + @description("The descriptions of the learningpath") description: Description, + @description("The introductions of the learningpath") introduction: Introduction, + @description("The full url to where the complete metainformation about the learningpath can be found") metaUrl: String, + @description("Url to where a cover photo can be found") coverPhotoUrl: Option[String], + @description("The duration of the learningpath in minutes") duration: Option[Int], + @description("The publishing status of the learningpath.") status: String, + @description("The date when this learningpath was last updated.") lastUpdated: NDLADate, + @description("Searchable tags for the learningpath") tags: LearningPathTags, + @description("The contributors of this learningpath") copyright: Copyright, + @description("A list of available languages for this audio") supportedLanguages: Seq[String], + @description("The id this learningpath is based on, if any") isBasedOn: Option[Long], + @description("Message that admins can place on a LearningPath for notifying a owner of issues with the LearningPath") message: Option[String] +) + +object LearningPathSummaryV2 { + implicit val encoder: Encoder[LearningPathSummaryV2] = deriveEncoder + implicit val decoder: Decoder[LearningPathSummaryV2] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathTags.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathTags.scala index c57f67f30b..e89d1052cb 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathTags.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathTags.scala @@ -8,12 +8,17 @@ package no.ndla.learningpathapi.model.api +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.language.model.WithLanguage -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description case class LearningPathTags( - @(ApiModelProperty @field)(description = "The searchable tags. Must be plain text") tags: Seq[String], - @(ApiModelProperty @field)(description = "ISO 639-1 code that represents the language used in tag") language: String + @description("The searchable tags. Must be plain text") tags: Seq[String], + @description("ISO 639-1 code that represents the language used in tag") language: String ) extends WithLanguage + +object LearningPathTags { + implicit val encoder: Encoder[LearningPathTags] = deriveEncoder + implicit val decoder: Decoder[LearningPathTags] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathTagsSummary.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathTagsSummary.scala index 2f2a5db0e1..9c495112f6 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathTagsSummary.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathTagsSummary.scala @@ -7,12 +7,17 @@ package no.ndla.learningpathapi.model.api -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description case class LearningPathTagsSummary( - @(ApiModelProperty @field)(description = "The chosen language. Default is 'nb'") language: String, - @(ApiModelProperty @field)(description = "The supported languages for these tags") supportedLanguages: Seq[String], - @(ApiModelProperty @field)(description = "The searchable tags. Must be plain text") tags: Seq[String] + @description("The chosen language. Default is 'nb'") language: String, + @description("The supported languages for these tags") supportedLanguages: Seq[String], + @description("The searchable tags. Must be plain text") tags: Seq[String] ) + +object LearningPathTagsSummary { + implicit val encoder: Encoder[LearningPathTagsSummary] = deriveEncoder + implicit val decoder: Decoder[LearningPathTagsSummary] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathV2.scala new file mode 100644 index 0000000000..fa17227ed5 --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningPathV2.scala @@ -0,0 +1,43 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import no.ndla.common.model.NDLADate +import sttp.tapir.Schema.annotations.description + +// format: off +@description("Meta information for a learningpath") +case class LearningPathV2( + @description("The unique id of the learningpath") id: Long, + @description("The revision number for this learningpath") revision: Int, + @description("The id this learningpath is based on, if any") isBasedOn: Option[Long], + @description("The title of the learningpath") title: Title, + @description("The description of the learningpath") description: Description, + @description("The full url to where the complete metainformation about the learningpath can be found") metaUrl: String, + @description("The learningsteps-summaries for this learningpath") learningsteps: Seq[LearningStepV2], + @description("The full url to where the learningsteps can be found") learningstepUrl: String, + @description("Information about where the cover photo can be found") coverPhoto: Option[CoverPhoto], + @description("The duration of the learningpath in minutes") duration: Option[Int], + @description("The publishing status of the learningpath") status: String, + @description("Verification status") verificationStatus: String, + @description("The date when this learningpath was last updated.") lastUpdated: NDLADate, + @description("Searchable tags for the learningpath") tags: LearningPathTags, + @description("Describes the copyright information for the learningpath") copyright: Copyright, + @description("True if authenticated user may edit this learningpath") canEdit: Boolean, + @description("The supported languages for this learningpath") supportedLanguages: Seq[String], + @description("Visible if administrator or owner of LearningPath") ownerId: Option[String], + @description("Message set by administrator. Visible if administrator or owner of LearningPath") message: Option[Message] +) + +object LearningPathV2 { + implicit val encoder: Encoder[LearningPathV2] = deriveEncoder + implicit val decoder: Decoder[LearningPathV2] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStep.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStep.scala deleted file mode 100644 index 01d34a1f24..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStep.scala +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import no.ndla.common.model.api.License -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field - -// format: off -@ApiModel(description = "Information about a learningstep") -case class LearningStepV2( - @(ApiModelProperty @field)(description = "The id of the learningstep") id: Long, - @(ApiModelProperty @field)(description = "The revision number for this learningstep") revision: Int, - @(ApiModelProperty @field)(description = "The sequence number for the step. The first step has seqNo 0.") seqNo: Int, - @(ApiModelProperty @field)(description = "The title of the learningstep") title: Title, - @(ApiModelProperty @field)(description = "The description of the learningstep") description: Option[Description], - @(ApiModelProperty @field)(description = "The embed content for the learningstep") embedUrl: Option[EmbedUrlV2], - @(ApiModelProperty @field)(description = "Determines if the title of the step should be displayed in viewmode") showTitle: Boolean, - @(ApiModelProperty @field)(description = "The type of the step", allowableValues = "INTRODUCTION,TEXT,QUIZ,TASK,MULTIMEDIA,SUMMARY,TEST") `type`: String, - @(ApiModelProperty @field)(description = "Describes the copyright information for the learningstep") license: Option[License], - @(ApiModelProperty @field)(description = "The full url to where the complete metainformation about the learningstep can be found") metaUrl: String, - @(ApiModelProperty @field)(description = "True if authenticated user may edit this learningstep") canEdit: Boolean, - @(ApiModelProperty @field)(description = "The status of the learningstep", allowableValues = "ACTIVE,DELETED") status: String, - @(ApiModelProperty @field)(description = "The supported languages of the learningstep") supportedLanguages: Seq[String] -) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepContainerSummary.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepContainerSummary.scala index 55e60611e8..d91126e831 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepContainerSummary.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepContainerSummary.scala @@ -8,14 +8,18 @@ package no.ndla.learningpathapi.model.api -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Summary of meta information for a learningstep including language and supported languages") +@description("Summary of meta information for a learningstep including language and supported languages") case class LearningStepContainerSummary( - @(ApiModelProperty @field)(description = "The chosen search language") language: String, - @(ApiModelProperty @field)(description = "The chosen search language") learningsteps: Seq[LearningStepSummaryV2], - @(ApiModelProperty @field)(description = "The chosen search language") supportedLanguages: Seq[String] + @description("The chosen search language") language: String, + @description("The chosen search language") learningsteps: Seq[LearningStepSummaryV2], + @description("The chosen search language") supportedLanguages: Seq[String] ) + +object LearningStepContainerSummary { + implicit val encoder: Encoder[LearningStepContainerSummary] = deriveEncoder + implicit val decoder: Decoder[LearningStepContainerSummary] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepSeqNo.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepSeqNo.scala index b1f0d9d111..5554c15581 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepSeqNo.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepSeqNo.scala @@ -8,11 +8,16 @@ package no.ndla.learningpathapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about the sequence number for a step") +@description("Information about the sequence number for a step") case class LearningStepSeqNo( - @(ApiModelProperty @field)(description = "The sequence number for the learningstep") seqNo: Int + @description("The sequence number for the learningstep") seqNo: Int ) + +object LearningStepSeqNo { + implicit val encoder: Encoder[LearningStepSeqNo] = deriveEncoder + implicit val decoder: Decoder[LearningStepSeqNo] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepStatus.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepStatus.scala index fa17a67a73..51edfbcb10 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepStatus.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepStatus.scala @@ -8,15 +8,16 @@ package no.ndla.learningpathapi.model.api -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Status information about a learningpath") +@description("Status information about a learningpath") case class LearningStepStatus( - @(ApiModelProperty @field)( - description = "The status of the learningstep", - allowableValues = "ACTIVE,DELETED" - ) status: String + @description("The status of the learningstep") status: String ) + +object LearningStepStatus { + implicit val encoder: Encoder[LearningStepStatus] = deriveEncoder + implicit val decoder: Decoder[LearningStepStatus] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepSummary.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepSummary.scala deleted file mode 100644 index 3f1ecb372d..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepSummary.scala +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field - -// format: off -@ApiModel(description = "Summary of meta information for a learningstep") -case class LearningStepSummaryV2( - @(ApiModelProperty @field)(description = "The id of the learningstep") id: Long, - @(ApiModelProperty @field)(description = "The sequence number for the step. The first step has seqNo 0.") seqNo: Int, - @(ApiModelProperty @field)(description = "The title of the learningstep") title: Title, - @(ApiModelProperty @field)(description = "The type of the step", allowableValues = "INTRODUCTION,TEXT,QUIZ,TASK,MULTIMEDIA,SUMMARY,TEST") `type`: String, - @(ApiModelProperty @field)(description = "The full url to where the complete metainformation about the learningstep can be found") metaUrl: String -) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepSummaryV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepSummaryV2.scala new file mode 100644 index 0000000000..f933a96357 --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepSummaryV2.scala @@ -0,0 +1,28 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description + +// format: off +@description("Summary of meta information for a learningstep") +case class LearningStepSummaryV2( + @description("The id of the learningstep") id: Long, + @description("The sequence number for the step. The first step has seqNo 0.") seqNo: Int, + @description("The title of the learningstep") title: Title, + @description("The type of the step") `type`: String, + @description("The full url to where the complete metainformation about the learningstep can be found") metaUrl: String +) + +object LearningStepSummaryV2{ + implicit val encoder: Encoder[LearningStepSummaryV2] = deriveEncoder + implicit val decoder: Decoder[LearningStepSummaryV2] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepV2.scala new file mode 100644 index 0000000000..19278a8a9e --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/LearningStepV2.scala @@ -0,0 +1,37 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import no.ndla.common.model.api.License +import sttp.tapir.Schema.annotations.description + +// format: off +@description("Information about a learningstep") +case class LearningStepV2( + @description("The id of the learningstep") id: Long, + @description("The revision number for this learningstep") revision: Int, + @description("The sequence number for the step. The first step has seqNo 0.") seqNo: Int, + @description("The title of the learningstep") title: Title, + @description("The description of the learningstep") description: Option[Description], + @description("The embed content for the learningstep") embedUrl: Option[EmbedUrlV2], + @description("Determines if the title of the step should be displayed in viewmode") showTitle: Boolean, + @description("The type of the step") `type`: String, + @description("Describes the copyright information for the learningstep") license: Option[License], + @description("The full url to where the complete metainformation about the learningstep can be found") metaUrl: String, + @description("True if authenticated user may edit this learningstep") canEdit: Boolean, + @description("The status of the learningstep") status: String, + @description("The supported languages of the learningstep") supportedLanguages: Seq[String] +) + +object LearningStepV2{ + implicit val encoder: Encoder[LearningStepV2] = deriveEncoder + implicit val decoder: Decoder[LearningStepV2] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Message.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Message.scala index 2107e9a7f7..bd9c625626 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Message.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Message.scala @@ -6,13 +6,18 @@ */ package no.ndla.learningpathapi.model.api +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Administrator message left on learningpaths") +@description("Administrator message left on learningpaths") case class Message( - @(ApiModelProperty @field)(description = "Message left on a learningpath by administrator") message: String, - @(ApiModelProperty @field)(description = "When the message was left") date: NDLADate + @description("Message left on a learningpath by administrator") message: String, + @description("When the message was left") date: NDLADate ) + +object Message { + implicit val encoder: Encoder[Message] = deriveEncoder + implicit val decoder: Decoder[Message] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewCopyLearningPath.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewCopyLearningPath.scala deleted file mode 100644 index 2583815604..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewCopyLearningPath.scala +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field - -@ApiModel(description = "Meta information for a new learningpath based on a copy") -case class NewCopyLearningPathV2( - @(ApiModelProperty @field)(description = "The titles of the learningpath") title: String, - @(ApiModelProperty @field)(description = "The descriptions of the learningpath") description: Option[String], - @(ApiModelProperty @field)(description = "The chosen language") language: String, - @(ApiModelProperty @field)(description = "Url to cover-photo in NDLA image-api.") coverPhotoMetaUrl: Option[String], - @(ApiModelProperty @field)( - description = "The duration of the learningpath in minutes. Must be greater than 0" - ) duration: Option[Int], - @(ApiModelProperty @field)(description = "Searchable tags for the learningpath") tags: Option[Seq[String]], - @(ApiModelProperty @field)( - description = "Describes the copyright information for the learningpath" - ) copyright: Option[Copyright] -) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewCopyLearningPathV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewCopyLearningPathV2.scala new file mode 100644 index 0000000000..e9fde27eb3 --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewCopyLearningPathV2.scala @@ -0,0 +1,29 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description + +@description("Meta information for a new learningpath based on a copy") +case class NewCopyLearningPathV2( + @description("The titles of the learningpath") title: String, + @description("The descriptions of the learningpath") description: Option[String], + @description("The chosen language") language: String, + @description("Url to cover-photo in NDLA image-api.") coverPhotoMetaUrl: Option[String], + @description("The duration of the learningpath in minutes. Must be greater than 0") duration: Option[Int], + @description("Searchable tags for the learningpath") tags: Option[Seq[String]], + @description("Describes the copyright information for the learningpath") copyright: Option[Copyright] +) + +object NewCopyLearningPathV2 { + implicit val encoder: Encoder[NewCopyLearningPathV2] = deriveEncoder + implicit val decoder: Decoder[NewCopyLearningPathV2] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningPath.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningPath.scala deleted file mode 100644 index 3719ab64d3..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningPath.scala +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field - -@ApiModel(description = "Meta information for a new learningpath") -case class NewLearningPathV2( - @(ApiModelProperty @field)(description = "The titles of the learningpath") title: String, - @(ApiModelProperty @field)(description = "The descriptions of the learningpath") description: String, - @(ApiModelProperty @field)(description = "Url to cover-photo in NDLA image-api.") coverPhotoMetaUrl: Option[String], - @(ApiModelProperty @field)( - description = "The duration of the learningpath in minutes. Must be greater than 0" - ) duration: Option[Int], - @(ApiModelProperty @field)(description = "Searchable tags for the learningpath") tags: Seq[String], - @(ApiModelProperty @field)(description = "The chosen language") language: String, - @(ApiModelProperty @field)( - description = "Describes the copyright information for the learningpath" - ) copyright: Copyright -) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningPathV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningPathV2.scala new file mode 100644 index 0000000000..2d3b5fe217 --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningPathV2.scala @@ -0,0 +1,29 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description + +@description("Meta information for a new learningpath") +case class NewLearningPathV2( + @description("The titles of the learningpath") title: String, + @description("The descriptions of the learningpath") description: String, + @description("Url to cover-photo in NDLA image-api.") coverPhotoMetaUrl: Option[String], + @description("The duration of the learningpath in minutes. Must be greater than 0") duration: Option[Int], + @description("Searchable tags for the learningpath") tags: Seq[String], + @description("The chosen language") language: String, + @description("Describes the copyright information for the learningpath") copyright: Copyright +) + +object NewLearningPathV2 { + implicit val encoder: Encoder[NewLearningPathV2] = deriveEncoder + implicit val decoder: Decoder[NewLearningPathV2] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningStep.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningStep.scala deleted file mode 100644 index b2bdb09503..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningStep.scala +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field - -@ApiModel(description = "Information about a new learningstep") -case class NewLearningStepV2( - @(ApiModelProperty @field)(description = "The titles of the learningstep") title: String, - @(ApiModelProperty @field)(description = "The descriptions of the learningstep") description: Option[String], - @(ApiModelProperty @field)(description = "The chosen language") language: String, - @(ApiModelProperty @field)(description = "The embed content for the learningstep") embedUrl: Option[EmbedUrlV2], - @(ApiModelProperty @field)( - description = "Determines if the title of the step should be displayed in viewmode" - ) showTitle: Boolean, - @(ApiModelProperty @field)( - description = "The type of the step", - allowableValues = "INTRODUCTION,TEXT,QUIZ,TASK,MULTIMEDIA,SUMMARY,TEST" - ) `type`: String, - @(ApiModelProperty @field)( - description = "Describes the copyright information for the learningstep" - ) license: Option[String] -) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningStepV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningStepV2.scala new file mode 100644 index 0000000000..4c9de7e519 --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/NewLearningStepV2.scala @@ -0,0 +1,29 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description + +@description("Information about a new learningstep") +case class NewLearningStepV2( + @description("The titles of the learningstep") title: String, + @description("The descriptions of the learningstep") description: Option[String], + @description("The chosen language") language: String, + @description("The embed content for the learningstep") embedUrl: Option[EmbedUrlV2], + @description("Determines if the title of the step should be displayed in viewmode") showTitle: Boolean, + @description("The type of the step") `type`: String, + @description("Describes the copyright information for the learningstep") license: Option[String] +) + +object NewLearningStepV2 { + implicit val encoder: Encoder[NewLearningStepV2] = deriveEncoder + implicit val decoder: Decoder[NewLearningStepV2] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/SearchParams.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/SearchParams.scala index 05abb487a1..5541869d9a 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/SearchParams.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/SearchParams.scala @@ -8,20 +8,26 @@ package no.ndla.learningpathapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} -import scala.annotation.meta.field +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description // format: off -@ApiModel(description = "The search parameters") +@description("The search parameters") case class SearchParams( - @(ApiModelProperty @field)(description = "The search query") query: Option[String], - @(ApiModelProperty @field)(description = "The ISO 639-1 language code describing language used in query-params") language: Option[String], - @(ApiModelProperty @field)(description = "The page number of the search hits to display.") page: Option[Int], - @(ApiModelProperty @field)(description = "The number of search hits to display for each page.") pageSize: Option[Int], - @(ApiModelProperty @field)(description = "Return only learning paths that have one of the provided ids") ids: List[Long], - @(ApiModelProperty @field)(description = "Return only learning paths that are tagged with this exact tag.") tag: Option[String], - @(ApiModelProperty @field)(description = "The sorting used on results. Default is by -relevance.") sort: Option[String], - @(ApiModelProperty @field)(description = "Return all matched learning paths whether they exist on selected language or not.") fallback: Option[Boolean], - @(ApiModelProperty @field)(description = "Return only learning paths that have the provided verification status.") verificationStatus: Option[String], - @(ApiModelProperty @field)(description = "A search context retrieved from the response header of a previous search.") scrollId: Option[String] + @description("The search query") query: Option[String], + @description("The ISO 639-1 language code describing language used in query-params") language: Option[String], + @description("The page number of the search hits to display.") page: Option[Int], + @description("The number of search hits to display for each page.") pageSize: Option[Int], + @description("Return only learning paths that have one of the provided ids") ids: Option[List[Long]], + @description("Return only learning paths that are tagged with this exact tag.") tag: Option[String], + @description("The sorting used on results. Default is by -relevance.") sort: Option[String], + @description("Return all matched learning paths whether they exist on selected language or not.") fallback: Option[Boolean], + @description("Return only learning paths that have the provided verification status.") verificationStatus: Option[String], + @description("A search context retrieved from the response header of a previous search.") scrollId: Option[String] ) + +object SearchParams{ + implicit val encoder: Encoder[SearchParams] = deriveEncoder + implicit val decoder: Decoder[SearchParams] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/SearchResult.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/SearchResult.scala deleted file mode 100644 index cac700834c..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/SearchResult.scala +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field - -@ApiModel(description = "Information about search-results") -case class SearchResultV2( - @(ApiModelProperty @field)(description = "The total number of learningpaths matching this query") totalCount: Long, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Option[Int], - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The chosen search language") language: String, - @(ApiModelProperty @field)(description = "The search results") results: Seq[LearningPathSummaryV2] -) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/SearchResultV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/SearchResultV2.scala new file mode 100644 index 0000000000..f797331b3d --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/SearchResultV2.scala @@ -0,0 +1,27 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description + +@description("Information about search-results") +case class SearchResultV2( + @description("The total number of learningpaths matching this query") totalCount: Long, + @description("For which page results are shown from") page: Option[Int], + @description("The number of results per page") pageSize: Int, + @description("The chosen search language") language: String, + @description("The search results") results: Seq[LearningPathSummaryV2] +) + +object SearchResultV2 { + implicit val encoder: Encoder[SearchResultV2] = deriveEncoder + implicit val decoder: Decoder[SearchResultV2] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Title.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Title.scala index 0bf44eacba..05f46dd896 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Title.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/Title.scala @@ -8,19 +8,21 @@ package no.ndla.learningpathapi.model.api +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.language.model.LanguageField -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Representation of a title") +@description("Representation of a title") case class Title( - @(ApiModelProperty @field)(description = "The title of the content. Must be plain text") title: String, - @(ApiModelProperty @field)( - description = "ISO 639-1 code that represents the language used in title" - ) language: String + @description("The title of the content. Must be plain text") title: String, + @description("ISO 639-1 code that represents the language used in title") language: String ) extends LanguageField[String] { override def value: String = title override def isEmpty: Boolean = title.isEmpty } + +object Title { + implicit val encoder: Encoder[Title] = deriveEncoder + implicit val decoder: Decoder[Title] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdateLearningPathStatus.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdateLearningPathStatus.scala index b4f7cb1d38..856f578dbc 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdateLearningPathStatus.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdateLearningPathStatus.scala @@ -6,18 +6,18 @@ */ package no.ndla.learningpathapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Status information about a learningpath") +// format: off +@description("Status information about a learningpath") case class UpdateLearningPathStatus( - @(ApiModelProperty @field)( - description = "The publishing status of the learningpath", - allowableValues = "PUBLISHED,PRIVATE,DELETED,UNLISTED,SUBMITTED" - ) status: String, - @(ApiModelProperty @field)( - description = - "Message that admins can place on a LearningPath for notifying a owner of issues with the LearningPath" - ) message: Option[String] + @description("The publishing status of the learningpath") status: String, + @description("Message that admins can place on a LearningPath for notifying a owner of issues with the LearningPath") message: Option[String] ) + +object UpdateLearningPathStatus { + implicit val encoder: Encoder[UpdateLearningPathStatus] = deriveEncoder + implicit val decoder: Decoder[UpdateLearningPathStatus] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningPath.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningPath.scala deleted file mode 100644 index 969b3d4c38..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningPath.scala +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field - -@ApiModel(description = "Meta information for a new learningpath") -case class UpdatedLearningPathV2( - @(ApiModelProperty @field)(description = "The revision number for this learningpath") revision: Int, - @(ApiModelProperty @field)(description = "The title of the learningpath") title: Option[String], - @(ApiModelProperty @field)(description = "The chosen language") language: String, - @(ApiModelProperty @field)(description = "The description of the learningpath") description: Option[String], - @(ApiModelProperty @field)(description = "Url to cover-photo in NDLA image-api.") coverPhotoMetaUrl: Option[String], - @(ApiModelProperty @field)( - description = "The duration of the learningpath in minutes. Must be greater than 0" - ) duration: Option[Int], - @(ApiModelProperty @field)(description = "Searchable tags for the learningpath") tags: Option[Seq[String]], - @(ApiModelProperty @field)( - description = "Describes the copyright information for the learningpath" - ) copyright: Option[Copyright], - @(ApiModelProperty @field)( - description = "Whether to delete a message connected to a learningpath by an administrator." - ) deleteMessage: Option[Boolean] -) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningPathV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningPathV2.scala new file mode 100644 index 0000000000..ed18f9f730 --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningPathV2.scala @@ -0,0 +1,33 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description + +@description("Meta information for a new learningpath") +case class UpdatedLearningPathV2( + @description("The revision number for this learningpath") revision: Int, + @description("The title of the learningpath") title: Option[String], + @description("The chosen language") language: String, + @description("The description of the learningpath") description: Option[String], + @description("Url to cover-photo in NDLA image-api.") coverPhotoMetaUrl: Option[String], + @description("The duration of the learningpath in minutes. Must be greater than 0") duration: Option[Int], + @description("Searchable tags for the learningpath") tags: Option[Seq[String]], + @description("Describes the copyright information for the learningpath") copyright: Option[Copyright], + @description("Whether to delete a message connected to a learningpath by an administrator.") deleteMessage: Option[ + Boolean + ] +) + +object UpdatedLearningPathV2 { + implicit val encoder: Encoder[UpdatedLearningPathV2] = deriveEncoder + implicit val decoder: Decoder[UpdatedLearningPathV2] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningStep.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningStep.scala deleted file mode 100644 index d7f5c73c3c..0000000000 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningStep.scala +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.model.api - -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field - -@ApiModel(description = "Information about a new learningstep") -case class UpdatedLearningStepV2( - @(ApiModelProperty @field)(description = "The revision number for this learningstep") revision: Int, - @(ApiModelProperty @field)(description = "The title of the learningstep") title: Option[String], - @(ApiModelProperty @field)(description = "The chosen language") language: String, - @(ApiModelProperty @field)(description = "The description of the learningstep") description: Option[String], - @(ApiModelProperty @field)(description = "The embed content for the learningstep") embedUrl: Option[EmbedUrlV2], - @(ApiModelProperty @field)( - description = "Determines if the title of the step should be displayed in viewmode" - ) showTitle: Option[Boolean], - @(ApiModelProperty @field)( - description = "The type of the step", - allowableValues = "INTRODUCTION,TEXT,QUIZ,TASK,MULTIMEDIA,SUMMARY,TEST" - ) `type`: Option[String], - @(ApiModelProperty @field)( - description = "Describes the copyright information for the learningstep" - ) license: Option[String] -) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningStepV2.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningStepV2.scala new file mode 100644 index 0000000000..7dad57b8a3 --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/UpdatedLearningStepV2.scala @@ -0,0 +1,30 @@ +/* + * Part of NDLA learningpath-api. + * Copyright (C) 2016 NDLA + * + * See LICENSE + * + */ + +package no.ndla.learningpathapi.model.api + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description + +@description("Information about a new learningstep") +case class UpdatedLearningStepV2( + @description("The revision number for this learningstep") revision: Int, + @description("The title of the learningstep") title: Option[String], + @description("The chosen language") language: String, + @description("The description of the learningstep") description: Option[String], + @description("The embed content for the learningstep") embedUrl: Option[EmbedUrlV2], + @description("Determines if the title of the step should be displayed in viewmode") showTitle: Option[Boolean], + @description("The type of the step") `type`: Option[String], + @description("Describes the copyright information for the learningstep") license: Option[String] +) + +object UpdatedLearningStepV2 { + implicit val encoder: Encoder[UpdatedLearningStepV2] = deriveEncoder + implicit val decoder: Decoder[UpdatedLearningStepV2] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/ValidationError.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/ValidationError.scala index 62b63689c1..f4c3bf4634 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/ValidationError.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/api/ValidationError.scala @@ -9,17 +9,14 @@ package no.ndla.learningpathapi.model.api import no.ndla.common.errors.ValidationMessage -import java.time.LocalDateTime - -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field +import java.time.LocalDateTime -@ApiModel(description = "Information about validation errors") +@description("Information about validation errors") case class ValidationError( - @(ApiModelProperty @field)(description = "Code stating the type of error") code: String, - @(ApiModelProperty @field)(description = "Description of the error") description: String, - @(ApiModelProperty @field)(description = "List of validation messages") messages: Seq[ValidationMessage], - @(ApiModelProperty @field)(description = "When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() + @description("Code stating the type of error") code: String, + @description("Description of the error") description: String, + @description("List of validation messages") messages: Seq[ValidationMessage], + @description("When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() ) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/Description.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/Description.scala index a28a46fd99..9df274c3c9 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/Description.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/Description.scala @@ -8,9 +8,16 @@ package no.ndla.learningpathapi.model.domain +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.language.model.LanguageField case class Description(description: String, language: String) extends LanguageField[String] { override def value: String = description override def isEmpty: Boolean = description.isEmpty } + +object Description { + implicit val encoder: Encoder[Description] = deriveEncoder + implicit val decoder: Decoder[Description] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/LearningPath.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/LearningPath.scala index ba6e2bb017..02b413fadd 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/LearningPath.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/LearningPath.scala @@ -8,12 +8,13 @@ package no.ndla.learningpathapi.model.domain +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.common.errors.{AccessDeniedException, ValidationException, ValidationMessage} import no.ndla.common.model.NDLADate import no.ndla.common.model.domain.learningpath.LearningpathCopyright import no.ndla.common.model.domain.{Tag, Title} import no.ndla.language.Language.getSupportedLanguages -import no.ndla.learningpathapi.Props import no.ndla.learningpathapi.model.domain.UserInfo.LearningpathTokenUser import no.ndla.learningpathapi.validation.DurationValidator import no.ndla.network.tapir.auth.TokenUser @@ -131,6 +132,9 @@ object LearningPathStatus extends Enumeration { def valueOfOrDefault(s: String): LearningPathStatus.Value = { valueOf(s).getOrElse(LearningPathStatus.PRIVATE) } + + implicit val encoder: Encoder[LearningPathStatus.Value] = Encoder.encodeEnumeration(LearningPathStatus) + implicit val decoder: Decoder[LearningPathStatus.Value] = Decoder.decodeEnumeration(LearningPathStatus) } object LearningPathVerificationStatus extends Enumeration { @@ -143,39 +147,39 @@ object LearningPathVerificationStatus extends Enumeration { def valueOfOrDefault(s: String): LearningPathVerificationStatus.Value = { valueOf(s).getOrElse(LearningPathVerificationStatus.EXTERNAL) } + implicit val encoder: Encoder[LearningPathVerificationStatus.Value] = + Encoder.encodeEnumeration(LearningPathVerificationStatus) + implicit val decoder: Decoder[LearningPathVerificationStatus.Value] = + Decoder.decodeEnumeration(LearningPathVerificationStatus) } -trait DBLearningPath { - this: Props => - - object DBLearningPath extends SQLSyntaxSupport[LearningPath] { - - val jsonSerializer: List[Serializer[_]] = List( - new EnumNameSerializer(LearningPathStatus), - new EnumNameSerializer(LearningPathVerificationStatus) - ) +object LearningPath extends SQLSyntaxSupport[LearningPath] { + implicit val encoder: Encoder[LearningPath] = deriveEncoder + implicit val decoder: Decoder[LearningPath] = deriveDecoder - val repositorySerializer = jsonSerializer :+ FieldSerializer[LearningPath]( - ignore("id").orElse(ignore("learningsteps")).orElse(ignore("externalId")).orElse(ignore("revision")) - ) + val jsonSerializer: List[Serializer[_]] = List( + new EnumNameSerializer(LearningPathStatus), + new EnumNameSerializer(LearningPathVerificationStatus) + ) - val jsonEncoder: Formats = DefaultFormats ++ jsonSerializer ++ JavaTimeSerializers.all + NDLADate.Json4sSerializer + val repositorySerializer = jsonSerializer :+ FieldSerializer[LearningPath]( + ignore("id").orElse(ignore("learningsteps")).orElse(ignore("externalId")).orElse(ignore("revision")) + ) - override val tableName = "learningpaths" - override val schemaName = Some(props.MetaSchema) + val jsonEncoder: Formats = DefaultFormats ++ jsonSerializer ++ JavaTimeSerializers.all + NDLADate.Json4sSerializer - def fromResultSet(lp: SyntaxProvider[LearningPath])(rs: WrappedResultSet): LearningPath = - fromResultSet(lp.resultName)(rs) + override val tableName = "learningpaths" - def fromResultSet(lp: ResultName[LearningPath])(rs: WrappedResultSet): LearningPath = { - implicit val formats: Formats = jsonEncoder ++ JavaTimeSerializers.all + NDLADate.Json4sSerializer - val meta = read[LearningPath](rs.string(lp.c("document"))) - meta.copy( - id = Some(rs.long(lp.c("id"))), - revision = Some(rs.int(lp.c("revision"))), - externalId = rs.stringOpt(lp.c("external_id")) - ) - } + def fromResultSet(lp: SyntaxProvider[LearningPath])(rs: WrappedResultSet): LearningPath = + fromResultSet(lp.resultName)(rs) + def fromResultSet(lp: ResultName[LearningPath])(rs: WrappedResultSet): LearningPath = { + implicit val formats: Formats = jsonEncoder ++ JavaTimeSerializers.all + NDLADate.Json4sSerializer + val meta = read[LearningPath](rs.string(lp.c("document"))) + meta.copy( + id = Some(rs.long(lp.c("id"))), + revision = Some(rs.int(lp.c("revision"))), + externalId = rs.stringOpt(lp.c("external_id")) + ) } } diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/LearningStep.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/LearningStep.scala index 0329632306..07737ae8b4 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/LearningStep.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/LearningStep.scala @@ -8,11 +8,13 @@ package no.ndla.learningpathapi.model.domain +import enumeratum._ +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.common.errors.{ValidationException, ValidationMessage} -import no.ndla.common.model.domain.learningpath.{EmbedType, EmbedUrl} import no.ndla.common.model.domain.Title +import no.ndla.common.model.domain.learningpath.{EmbedType, EmbedUrl} import no.ndla.language.Language.getSupportedLanguages -import no.ndla.learningpathapi.Props import org.json4s.FieldSerializer._ import org.json4s._ import org.json4s.ext.EnumNameSerializer @@ -43,11 +45,9 @@ case class LearningStep( } } -import enumeratum._ - sealed abstract class StepStatus(override val entryName: String) extends EnumEntry -object StepStatus extends Enum[StepStatus] { +object StepStatus extends Enum[StepStatus] with CirceEnum[StepStatus] { case object ACTIVE extends StepStatus("ACTIVE") case object DELETED extends StepStatus("DELETED") @@ -90,52 +90,52 @@ object StepType extends Enumeration { def valueOfOrDefault(s: String): StepType.Value = { valueOf(s).getOrElse(StepType.TEXT) } + implicit val encoder: Encoder[StepType.Value] = Encoder.encodeEnumeration(StepType) + implicit val decoder: Decoder[StepType.Value] = Decoder.decodeEnumeration(StepType) } -trait DBLearningStep { - this: Props => - - object DBLearningStep extends SQLSyntaxSupport[LearningStep] { - - val jsonSerializer: List[Serializer[_]] = List( - new EnumNameSerializer(StepType), - Json4s.serializer(StepStatus), - new EnumNameSerializer(EmbedType) +object LearningStep extends SQLSyntaxSupport[LearningStep] { + + implicit val encoder: Encoder[LearningStep] = deriveEncoder + implicit val decoder: Decoder[LearningStep] = deriveDecoder + + val jsonSerializer: List[Serializer[_]] = List( + new EnumNameSerializer(StepType), + Json4s.serializer(StepStatus), + new EnumNameSerializer(EmbedType) + ) + + val repositorySerializer = jsonSerializer :+ FieldSerializer[LearningStep]( + serializer = ignore("id").orElse(ignore("learningPathId")).orElse(ignore("externalId")).orElse(ignore("revision")) + ) + + val jsonEncoder = DefaultFormats ++ jsonSerializer + + override val tableName = "learningsteps" + + def fromResultSet(ls: SyntaxProvider[LearningStep])(rs: WrappedResultSet): LearningStep = + fromResultSet(ls.resultName)(rs) + + def fromResultSet(ls: ResultName[LearningStep])(rs: WrappedResultSet): LearningStep = { + implicit val formats = jsonEncoder + + val meta = read[LearningStep](rs.string(ls.c("document"))) + LearningStep( + Some(rs.long(ls.c("id"))), + Some(rs.int(ls.c("revision"))), + rs.stringOpt(ls.c("external_id")), + Some(rs.long(ls.c("learning_path_id"))), + meta.seqNo, + meta.title, + meta.description, + meta.embedUrl, + meta.`type`, + meta.license, + meta.showTitle, + meta.status ) - - val repositorySerializer = jsonSerializer :+ FieldSerializer[LearningStep]( - serializer = ignore("id").orElse(ignore("learningPathId")).orElse(ignore("externalId")).orElse(ignore("revision")) - ) - - val jsonEncoder = DefaultFormats ++ jsonSerializer - - override val tableName = "learningsteps" - override val schemaName = Some(props.MetaSchema) - - def fromResultSet(ls: SyntaxProvider[LearningStep])(rs: WrappedResultSet): LearningStep = - fromResultSet(ls.resultName)(rs) - - def fromResultSet(ls: ResultName[LearningStep])(rs: WrappedResultSet): LearningStep = { - implicit val formats = jsonEncoder - - val meta = read[LearningStep](rs.string(ls.c("document"))) - LearningStep( - Some(rs.long(ls.c("id"))), - Some(rs.int(ls.c("revision"))), - rs.stringOpt(ls.c("external_id")), - Some(rs.long(ls.c("learning_path_id"))), - meta.seqNo, - meta.title, - meta.description, - meta.embedUrl, - meta.`type`, - meta.license, - meta.showTitle, - meta.status - ) - } - - def opt(ls: ResultName[LearningStep])(rs: WrappedResultSet): Option[LearningStep] = - rs.longOpt(ls.c("id")).map(_ => fromResultSet(ls)(rs)) } + + def opt(ls: ResultName[LearningStep])(rs: WrappedResultSet): Option[LearningStep] = + rs.longOpt(ls.c("id")).map(_ => fromResultSet(ls)(rs)) } diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/Message.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/Message.scala index 93ef3a72dc..1f2d0c6924 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/Message.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/Message.scala @@ -7,6 +7,13 @@ package no.ndla.learningpathapi.model.domain +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate case class Message(message: String, adminName: String, date: NDLADate) + +object Message { + implicit val encoder: Encoder[Message] = deriveEncoder + implicit val decoder: Decoder[Message] = deriveDecoder +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/NDLAErrors.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/NDLAErrors.scala index f0827a4c33..1a37087d98 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/NDLAErrors.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/NDLAErrors.scala @@ -11,7 +11,6 @@ package no.ndla.learningpathapi.model.domain class OptimisticLockException(message: String) extends RuntimeException(message) class ImportException(message: String) extends RuntimeException(message) case class ElasticIndexingException(message: String) extends RuntimeException(message) -class ResultWindowTooLargeException(message: String) extends RuntimeException(message) case class SearchException(message: String) extends RuntimeException(message) case class TaxonomyUpdateException(message: String) extends RuntimeException(message) case class InvalidOembedResponse(message: String) extends RuntimeException(message) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/UserInfo.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/UserInfo.scala index 4d574220bd..5b96ff80f4 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/UserInfo.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/model/domain/UserInfo.scala @@ -11,17 +11,13 @@ import no.ndla.common.errors.AccessDeniedException import no.ndla.network.tapir.auth.Permission.{LEARNINGPATH_API_ADMIN, LEARNINGPATH_API_PUBLISH, LEARNINGPATH_API_WRITE} import no.ndla.network.tapir.auth.TokenUser -import javax.servlet.http.HttpServletRequest import scala.util.{Failure, Success} object UserInfo { - def getUserOrPublic(implicit request: HttpServletRequest): TokenUser = { - TokenUser.fromScalatraRequest(request).getOrElse(TokenUser.PublicUser) - } - def getWithUserIdOrAdmin(implicit request: HttpServletRequest) = - TokenUser.fromScalatraRequest(request) match { - case Success(user) if user.isAdmin => Success(user) - case Success(user) if user.jwt.ndla_id.isDefined => Success(user) + def getWithUserIdOrAdmin(user: TokenUser) = + user match { + case user if user.isAdmin => Success(user) + case user if user.jwt.ndla_id.isDefined => Success(user) case _ => Failure(AccessDeniedException("You do not have access to the requested resource.")) } diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/package.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/package.scala new file mode 100644 index 0000000000..f0192e322c --- /dev/null +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/package.scala @@ -0,0 +1,14 @@ +/* + * Part of NDLA learningpath-api + * Copyright (C) 2024 NDLA + * + * See LICENSE + */ + +package no.ndla + +import sttp.tapir.server.jdkhttp.Id + +package object learningpathapi { + type Eff[A] = Id[A] +} diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/repository/LearningPathRepositoryComponent.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/repository/LearningPathRepositoryComponent.scala index 2826c7a763..fb26259c65 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/repository/LearningPathRepositoryComponent.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/repository/LearningPathRepositoryComponent.scala @@ -26,7 +26,7 @@ import java.util.UUID import scala.util.Try trait LearningPathRepositoryComponent extends StrictLogging { - this: DataSource with DBLearningPath with DBLearningStep with Props => + this: DataSource with Props => val learningPathRepository: LearningPathRepository def inTransaction[A](work: DBSession => A)(implicit session: DBSession = null): A = { @@ -42,8 +42,8 @@ trait LearningPathRepositoryComponent extends StrictLogging { class LearningPathRepository { implicit val formats: Formats = DefaultFormats ++ - DBLearningPath.jsonSerializer ++ - DBLearningStep.jsonSerializer ++ + LearningPath.jsonSerializer ++ + LearningStep.jsonSerializer ++ JavaTimeSerializers.all + NDLADate.Json4sSerializer @@ -76,18 +76,18 @@ trait LearningPathRepositoryComponent extends StrictLogging { } def learningStepsFor(learningPathId: Long)(implicit session: DBSession = ReadOnlyAutoSession): Seq[LearningStep] = { - val ls = DBLearningStep.syntax("ls") - sql"select ${ls.result.*} from ${DBLearningStep.as(ls)} where ${ls.learningPathId} = $learningPathId" - .map(DBLearningStep.fromResultSet(ls.resultName)) + val ls = LearningStep.syntax("ls") + sql"select ${ls.result.*} from ${LearningStep.as(ls)} where ${ls.learningPathId} = $learningPathId" + .map(LearningStep.fromResultSet(ls.resultName)) .list() } def learningStepWithId(learningPathId: Long, learningStepId: Long)(implicit session: DBSession = ReadOnlyAutoSession ): Option[LearningStep] = { - val ls = DBLearningStep.syntax("ls") - sql"select ${ls.result.*} from ${DBLearningStep.as(ls)} where ${ls.learningPathId} = $learningPathId and ${ls.id} = $learningStepId" - .map(DBLearningStep.fromResultSet(ls.resultName)) + val ls = LearningStep.syntax("ls") + sql"select ${ls.result.*} from ${LearningStep.as(ls)} where ${ls.learningPathId} = $learningPathId and ${ls.id} = $learningStepId" + .map(LearningStep.fromResultSet(ls.resultName)) .single() } @@ -97,9 +97,9 @@ trait LearningPathRepositoryComponent extends StrictLogging { if (externalId.isEmpty || learningPathId.isEmpty) { None } else { - val ls = DBLearningStep.syntax("ls") - sql"select ${ls.result.*} from ${DBLearningStep.as(ls)} where ${ls.externalId} = ${externalId.get} and ${ls.learningPathId} = ${learningPathId.get}" - .map(DBLearningStep.fromResultSet(ls.resultName)) + val ls = LearningStep.syntax("ls") + sql"select ${ls.result.*} from ${LearningStep.as(ls)} where ${ls.externalId} = ${externalId.get} and ${ls.learningPathId} = ${learningPathId.get}" + .map(LearningStep.fromResultSet(ls.resultName)) .single() } } @@ -149,9 +149,9 @@ trait LearningPathRepositoryComponent extends StrictLogging { def idAndimportIdOfLearningpath( externalId: String )(implicit session: DBSession = AutoSession): Option[(Long, Option[String])] = { - val lp = DBLearningPath.syntax("lp") + val lp = LearningPath.syntax("lp") sql"""select id, import_id - from ${DBLearningPath.as(lp)} + from ${LearningPath.as(lp)} where lp.document is not NULL and lp.external_id = $externalId""" .map(rs => (rs.long("id"), rs.stringOpt("import_id"))) .single() @@ -258,16 +258,16 @@ trait LearningPathRepositoryComponent extends StrictLogging { def learningPathsWithIdBetween(min: Long, max: Long)(implicit session: DBSession = ReadOnlyAutoSession ): List[LearningPath] = { - val (lp, ls) = (DBLearningPath.syntax("lp"), DBLearningStep.syntax("ls")) + val (lp, ls) = (LearningPath.syntax("lp"), LearningStep.syntax("ls")) val status = LearningPathStatus.PUBLISHED.toString sql"""select ${lp.result.*}, ${ls.result.*} - from ${DBLearningPath.as(lp)} - left join ${DBLearningStep.as(ls)} on ${lp.id} = ${ls.learningPathId} + from ${LearningPath.as(lp)} + left join ${LearningStep.as(ls)} on ${lp.id} = ${ls.learningPathId} where lp.document->>'status' = $status and lp.id between $min and $max""" - .one(DBLearningPath.fromResultSet(lp.resultName)) - .toMany(DBLearningStep.opt(ls.resultName)) + .one(LearningPath.fromResultSet(lp.resultName)) + .toMany(LearningStep.opt(ls.resultName)) .map { (learningpath, learningsteps) => learningpath.copy(learningsteps = Some(learningsteps.toSeq)) } @@ -322,11 +322,11 @@ trait LearningPathRepositoryComponent extends StrictLogging { private def learningPathsWhere( whereClause: SQLSyntax )(implicit session: DBSession = ReadOnlyAutoSession): List[LearningPath] = { - val (lp, ls) = (DBLearningPath.syntax("lp"), DBLearningStep.syntax("ls")) - sql"select ${lp.result.*}, ${ls.result.*} from ${DBLearningPath.as(lp)} left join ${DBLearningStep + val (lp, ls) = (LearningPath.syntax("lp"), LearningStep.syntax("ls")) + sql"select ${lp.result.*}, ${ls.result.*} from ${LearningPath.as(lp)} left join ${LearningStep .as(ls)} on ${lp.id} = ${ls.learningPathId} where $whereClause" - .one(DBLearningPath.fromResultSet(lp.resultName)) - .toMany(DBLearningStep.opt(ls.resultName)) + .one(LearningPath.fromResultSet(lp.resultName)) + .toMany(LearningStep.opt(ls.resultName)) .map { (learningpath, learningsteps) => learningpath.copy(learningsteps = Some(learningsteps.filter(_.status == StepStatus.ACTIVE).toSeq)) } @@ -336,11 +336,11 @@ trait LearningPathRepositoryComponent extends StrictLogging { private def learningPathWhere( whereClause: SQLSyntax )(implicit session: DBSession = ReadOnlyAutoSession): Option[LearningPath] = { - val (lp, ls) = (DBLearningPath.syntax("lp"), DBLearningStep.syntax("ls")) - sql"select ${lp.result.*}, ${ls.result.*} from ${DBLearningPath.as(lp)} left join ${DBLearningStep + val (lp, ls) = (LearningPath.syntax("lp"), LearningStep.syntax("ls")) + sql"select ${lp.result.*}, ${ls.result.*} from ${LearningPath.as(lp)} left join ${LearningStep .as(ls)} on ${lp.id} = ${ls.learningPathId} where $whereClause" - .one(DBLearningPath.fromResultSet(lp.resultName)) - .toMany(DBLearningStep.opt(ls.resultName)) + .one(LearningPath.fromResultSet(lp.resultName)) + .toMany(LearningStep.opt(ls.resultName)) .map { (learningpath, learningsteps) => learningpath.copy(learningsteps = Some(learningsteps.filter(_.status == StepStatus.ACTIVE).toSeq)) } @@ -350,18 +350,18 @@ trait LearningPathRepositoryComponent extends StrictLogging { def pageWithIds(ids: Seq[Long], pageSize: Int, offset: Int)(implicit session: DBSession = ReadOnlyAutoSession ): List[LearningPath] = { - val (lp, ls) = (DBLearningPath.syntax("lp"), DBLearningStep.syntax("ls")) + val (lp, ls) = (LearningPath.syntax("lp"), LearningStep.syntax("ls")) val lps = SubQuery.syntax("lps").include(lp) sql""" select ${lps.resultAll}, ${ls.resultAll} from (select ${lp.resultAll} - from ${DBLearningPath.as(lp)} + from ${LearningPath.as(lp)} where ${lp.c("id")} in ($ids) limit $pageSize offset $offset) lps - left join ${DBLearningStep.as(ls)} on ${lps(lp).id} = ${ls.learningPathId} + left join ${LearningStep.as(ls)} on ${lps(lp).id} = ${ls.learningPathId} """ - .one(DBLearningPath.fromResultSet(lps(lp).resultName)) - .toMany(DBLearningStep.opt(ls.resultName)) + .one(LearningPath.fromResultSet(lps(lp).resultName)) + .toMany(LearningStep.opt(ls.resultName)) .map { (learningpath, learningsteps) => learningpath.copy(learningsteps = Some(learningsteps.filter(_.status == StepStatus.ACTIVE).toSeq)) } @@ -371,18 +371,18 @@ trait LearningPathRepositoryComponent extends StrictLogging { def getAllLearningPathsByPage(pageSize: Int, offset: Int)(implicit session: DBSession = ReadOnlyAutoSession ): List[LearningPath] = { - val (lp, ls) = (DBLearningPath.syntax("lp"), DBLearningStep.syntax("ls")) + val (lp, ls) = (LearningPath.syntax("lp"), LearningStep.syntax("ls")) val lps = SubQuery.syntax("lps").include(lp) sql""" select ${lps.resultAll}, ${ls.resultAll} from (select ${lp.resultAll}, ${lp.id} as row_id - from ${DBLearningPath.as(lp)} + from ${LearningPath.as(lp)} limit $pageSize offset $offset) lps - left join ${DBLearningStep.as(ls)} on ${lps(lp).id} = ${ls.learningPathId} + left join ${LearningStep.as(ls)} on ${lps(lp).id} = ${ls.learningPathId} order by row_id """ - .one(DBLearningPath.fromResultSet(lps(lp).resultName)) - .toMany(DBLearningStep.opt(ls.resultName)) + .one(LearningPath.fromResultSet(lps(lp).resultName)) + .toMany(LearningStep.opt(ls.resultName)) .map { (learningpath, learningsteps) => learningpath.copy(learningsteps = Some(learningsteps.filter(_.status == StepStatus.ACTIVE).toSeq)) } @@ -392,19 +392,19 @@ trait LearningPathRepositoryComponent extends StrictLogging { def getPublishedLearningPathByPage(pageSize: Int, offset: Int)(implicit session: DBSession = ReadOnlyAutoSession ): List[LearningPath] = { - val (lp, ls) = (DBLearningPath.syntax("lp"), DBLearningStep.syntax("ls")) + val (lp, ls) = (LearningPath.syntax("lp"), LearningStep.syntax("ls")) val lps = SubQuery.syntax("lps").include(lp) sql""" select ${lps.resultAll}, ${ls.resultAll} from (select ${lp.resultAll}, ${lp.id} as row_id - from ${DBLearningPath.as(lp)} + from ${LearningPath.as(lp)} where document#>>'{status}' = ${LearningPathStatus.PUBLISHED.toString} limit $pageSize offset $offset) lps - left join ${DBLearningStep.as(ls)} on ${lps(lp).id} = ${ls.learningPathId} + left join ${LearningStep.as(ls)} on ${lps(lp).id} = ${ls.learningPathId} order by row_id """ - .one(DBLearningPath.fromResultSet(lps(lp).resultName)) - .toMany(DBLearningStep.opt(ls.resultName)) + .one(LearningPath.fromResultSet(lps(lp).resultName)) + .toMany(LearningStep.opt(ls.resultName)) .map { (learningpath, learningsteps) => learningpath.copy(learningsteps = Some(learningsteps.filter(_.status == StepStatus.ACTIVE).toSeq)) } @@ -418,16 +418,16 @@ trait LearningPathRepositoryComponent extends StrictLogging { } def publishedLearningPathCount(implicit session: DBSession = ReadOnlyAutoSession): Long = { - val (lp, _) = (DBLearningPath.syntax("lp"), DBLearningStep.syntax("ls")) - sql"select count(*) from ${DBLearningPath.as(lp)} where document#>>'{status}' = ${LearningPathStatus.PUBLISHED.toString}" + val (lp, _) = (LearningPath.syntax("lp"), LearningStep.syntax("ls")) + sql"select count(*) from ${LearningPath.as(lp)} where document#>>'{status}' = ${LearningPathStatus.PUBLISHED.toString}" .map(rs => rs.long("count")) .single() .getOrElse(0) } def learningPathCount(implicit session: DBSession = ReadOnlyAutoSession): Long = { - val (lp, _) = (DBLearningPath.syntax("lp"), DBLearningStep.syntax("ls")) - sql"select count(*) from ${DBLearningPath.as(lp)}" + val (lp, _) = (LearningPath.syntax("lp"), LearningStep.syntax("ls")) + sql"select count(*) from ${LearningPath.as(lp)}" .map(rs => rs.long("count")) .single() .getOrElse(0) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchService.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchService.scala index 0abcad651e..adbfafb470 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchService.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchService.scala @@ -186,7 +186,7 @@ trait SearchService extends StrictLogging { logger.info( s"Max supported results are $ElasticSearchIndexMaxResultWindow, user requested $requestedResultWindow" ) - Failure(new ResultWindowTooLargeException(ErrorHelpers.WindowTooLargeError.description)) + Failure(LearningpathHelpers.ResultWindowTooLargeException()) } else { val searchToExecute = search(props.SearchIndex) .size(numResults) diff --git a/learningpath-api/src/test/scala/no/ndla/learningpathapi/TestEnvironment.scala b/learningpath-api/src/test/scala/no/ndla/learningpathapi/TestEnvironment.scala index 9bce2a06ab..cca5ee939c 100644 --- a/learningpath-api/src/test/scala/no/ndla/learningpathapi/TestEnvironment.scala +++ b/learningpath-api/src/test/scala/no/ndla/learningpathapi/TestEnvironment.scala @@ -10,31 +10,22 @@ package no.ndla.learningpathapi import com.zaxxer.hikari.HikariDataSource import no.ndla.common.Clock -import no.ndla.learningpathapi.controller.{ - HealthController, - InternController, - LearningpathControllerV2, - NdlaController, - StatsController -} +import no.ndla.learningpathapi.controller.{InternController, LearningpathControllerV2, StatsController} import no.ndla.learningpathapi.integration._ import no.ndla.learningpathapi.model.api.ErrorHelpers -import no.ndla.learningpathapi.model.domain.{DBLearningPath, DBLearningStep} import no.ndla.learningpathapi.repository.LearningPathRepositoryComponent import no.ndla.learningpathapi.service._ import no.ndla.learningpathapi.service.search.{SearchConverterServiceComponent, SearchIndexService, SearchService} import no.ndla.learningpathapi.validation._ import no.ndla.network.NdlaClient import no.ndla.network.clients.{FeideApiClient, RedisClient} -import no.ndla.network.scalatra.{NdlaControllerBase, NdlaSwaggerSupport} +import no.ndla.network.tapir.{NdlaMiddleware, Routes, Service} import no.ndla.search.{BaseIndexService, Elastic4sClient} import org.mockito.scalatest.MockitoSugar trait TestEnvironment extends LearningpathControllerV2 with StatsController - with NdlaControllerBase - with NdlaSwaggerSupport with LearningPathRepositoryComponent with FeideApiClient with ReadService @@ -53,23 +44,20 @@ trait TestEnvironment with DataSource with MockitoSugar with Clock - with HealthController with LanguageValidator with LearningPathValidator with LearningStepValidator with TitleValidator with TextValidator with UrlValidator - with DBLearningPath with MyNDLAApiClient - with DBLearningStep - with NdlaController with ErrorHelpers with Props with InternController with DBMigrator - with LearningpathApiInfo - with RedisClient { + with RedisClient + with NdlaMiddleware + with Routes[Eff] { val props = new LearningpathApiProperties val migrator: DBMigrator = mock[DBMigrator] @@ -90,7 +78,6 @@ trait TestEnvironment val languageValidator: LanguageValidator = mock[LanguageValidator] val learningpathControllerV2: LearningpathControllerV2 = mock[LearningpathControllerV2] val statsController: StatsController = mock[StatsController] - val healthController: HealthController = mock[HealthController] val internController: InternController = mock[InternController] val learningStepValidator: LearningStepValidator = mock[LearningStepValidator] val learningPathValidator: LearningPathValidator = mock[LearningPathValidator] @@ -102,6 +89,8 @@ trait TestEnvironment val redisClient: RedisClient = mock[RedisClient] val myndlaApiClient: MyNDLAApiClient = mock[MyNDLAApiClient] + val services: List[Service[Eff]] = List.empty + def resetMocks(): Unit = { reset( dataSource, diff --git a/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/HealthControllerTest.scala b/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/HealthControllerTest.scala deleted file mode 100644 index 1e2c1a8d5a..0000000000 --- a/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/HealthControllerTest.scala +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Part of NDLA learningpath-api. - * Copyright (C) 2016 NDLA - * - * See LICENSE - * - */ - -package no.ndla.learningpathapi.controller - -import no.ndla.learningpathapi.{TestEnvironment, UnitSuite} -import org.scalatra.test.scalatest.ScalatraFunSuite - -class HealthControllerTest extends UnitSuite with TestEnvironment with ScalatraFunSuite { - - lazy val controller = new HealthController - controller.setWarmedUp() - addServlet(controller, "/*") - - test("That /health returns 200 ok") { - get("/") { - status should equal(200) - } - } -} diff --git a/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/InternControllerTest.scala b/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/InternControllerTest.scala index 84529956bc..114bce6018 100644 --- a/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/InternControllerTest.scala +++ b/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/InternControllerTest.scala @@ -7,28 +7,38 @@ package no.ndla.learningpathapi.controller -import no.ndla.learningpathapi.{TestEnvironment, UnitSuite} +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import no.ndla.learningpathapi.{Eff, TestEnvironment, UnitSuite} +import no.ndla.network.tapir.Service import org.json4s.Formats -import org.scalatra.test.scalatest.ScalatraFunSuite import scalikejdbc.DBSession +import sttp.client3.quick._ import scala.util.{Failure, Success} -class InternControllerTest extends UnitSuite with TestEnvironment with ScalatraFunSuite { +class InternControllerTest extends UnitSuite with TestEnvironment { - implicit val jsonFormats: Formats = org.json4s.DefaultFormats - val swagger = new LearningpathSwagger + implicit val jsonFormats: Formats = org.json4s.DefaultFormats + val serverPort: Int = findFreePort + val controller = new InternController + override val services: List[Service[Eff]] = List(controller) - lazy val controller = new InternController()(swagger) - addServlet(controller, "/*") + override def beforeAll(): Unit = { + IO { Routes.startJdkServer("InternControllerTest", serverPort) {} }.unsafeRunAndForget() + Thread.sleep(1000) + } test("that id with value 404 gives OK") { resetMocks() when(learningPathRepository.getIdFromExternalId(any[String])(any[DBSession])).thenReturn(Some(404L)) - get("/id/1234") { - status should equal(200) - } + simpleHttpClient + .send( + quickRequest.get(uri"http://localhost:$serverPort/intern/id/1234") + ) + .code + .code should be(200) } test("That DELETE /index removes all indexes") { @@ -37,10 +47,12 @@ class InternControllerTest extends UnitSuite with TestEnvironment with ScalatraF doReturn(Success(""), Nil: _*).when(searchIndexService).deleteIndexWithName(Some("index1")) doReturn(Success(""), Nil: _*).when(searchIndexService).deleteIndexWithName(Some("index2")) doReturn(Success(""), Nil: _*).when(searchIndexService).deleteIndexWithName(Some("index3")) - delete("/index") { - status should equal(200) - body should equal("Deleted 3 indexes") - } + + val res = simpleHttpClient.send( + quickRequest.delete(uri"http://localhost:$serverPort/intern/index") + ) + res.code.code should be(200) + res.body should be("Deleted 3 indexes") verify(searchIndexService).findAllIndexes(props.SearchIndex) verify(searchIndexService).deleteIndexWithName(Some("index1")) verify(searchIndexService).deleteIndexWithName(Some("index2")) @@ -56,10 +68,11 @@ class InternControllerTest extends UnitSuite with TestEnvironment with ScalatraF doReturn(Success(""), Nil: _*).when(searchIndexService).deleteIndexWithName(Some("index1")) doReturn(Success(""), Nil: _*).when(searchIndexService).deleteIndexWithName(Some("index2")) doReturn(Success(""), Nil: _*).when(searchIndexService).deleteIndexWithName(Some("index3")) - delete("/index") { - status should equal(500) - body should equal("Failed to find indexes") - } + val res = simpleHttpClient.send( + quickRequest.delete(uri"http://localhost:$serverPort/intern/index") + ) + res.code.code should be(500) + res.body should equal("Failed to find indexes") verify(searchIndexService, never).deleteIndexWithName(any[Option[String]]) } @@ -73,14 +86,24 @@ class InternControllerTest extends UnitSuite with TestEnvironment with ScalatraF .when(searchIndexService) .deleteIndexWithName(Some("index2")) doReturn(Success(""), Nil: _*).when(searchIndexService).deleteIndexWithName(Some("index3")) - delete("/index") { - status should equal(500) - body should equal( - "Failed to delete 1 index: No index with name 'index2' exists. 2 indexes were deleted successfully." - ) - } + val res = simpleHttpClient.send( + quickRequest.delete(uri"http://localhost:$serverPort/intern/index") + ) + res.code.code should be(500) + res.body should be( + "Failed to delete 1 index: No index with name 'index2' exists. 2 indexes were deleted successfully." + ) verify(searchIndexService).deleteIndexWithName(Some("index1")) verify(searchIndexService).deleteIndexWithName(Some("index2")) verify(searchIndexService).deleteIndexWithName(Some("index3")) } + + test("That no endpoints are shadowed") { + import sttp.tapir.testing.EndpointVerifier + val errors = EndpointVerifier(controller.endpoints.map(_.endpoint)) + if (errors.nonEmpty) { + val errString = errors.map(e => e.toString).mkString("\n\t- ", "\n\t- ", "") + fail(s"Got errors when verifying ${controller.serviceName} controller:$errString") + } + } } diff --git a/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2Test.scala b/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2Test.scala index a272efcdda..4ef82af0bf 100644 --- a/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2Test.scala +++ b/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/LearningpathControllerV2Test.scala @@ -8,32 +8,46 @@ package no.ndla.learningpathapi.controller -import no.ndla.common.errors.ValidationException +import cats.effect.IO +import cats.effect.unsafe.implicits.global import no.ndla.common.model.{NDLADate, api => commonApi} import no.ndla.learningpathapi.TestData.searchSettings import no.ndla.learningpathapi.integration.Node import no.ndla.learningpathapi.model.api.{LearningPathSummaryV2, SearchResultV2} import no.ndla.learningpathapi.model.{api, domain} import no.ndla.learningpathapi.model.domain._ -import no.ndla.learningpathapi.{TestData, TestEnvironment, UnitSuite} +import no.ndla.learningpathapi.{Eff, TestData, TestEnvironment, UnitSuite} import no.ndla.mapping.License.getLicenses import no.ndla.myndla.model.domain.InvalidStatusException +import no.ndla.network.tapir.Service import no.ndla.network.tapir.auth.TokenUser import org.json4s.Formats import org.json4s.ext.JavaTimeSerializers import org.json4s.native.Serialization._ import org.mockito.ArgumentMatchers._ -import org.mockito.Strictness -import org.scalatra.test.scalatest.ScalatraFunSuite -import javax.servlet.http.HttpServletRequest -import scala.jdk.CollectionConverters._ import scala.util.{Failure, Success} +import sttp.client3.quick._ -class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with ScalatraFunSuite { +class LearningpathControllerV2Test extends UnitSuite with TestEnvironment { implicit val formats: Formats = org.json4s.DefaultFormats ++ JavaTimeSerializers.all + NDLADate.Json4sSerializer - implicit val swagger: LearningpathSwagger = new LearningpathSwagger + val serverPort: Int = findFreePort + val controller = new LearningpathControllerV2 + override val services: List[Service[Eff]] = List(controller) + + override def beforeAll(): Unit = { + IO { Routes.startJdkServer("LearningpathControllerV2Test", serverPort) {} }.unsafeRunAndForget() + Thread.sleep(1000) + } + + override def beforeEach(): Unit = { + resetMocks() + when(clock.now()).thenCallRealMethod() + when(languageValidator.validate(any[String], any[String], any[Boolean])) + .thenReturn(None) + when(searchConverterService.asApiSearchResult(any)).thenCallRealMethod() + } val copyright: api.Copyright = api.Copyright(commonApi.License("by-sa", None, None), List()) @@ -55,15 +69,6 @@ class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with S None ) - lazy val controller = new LearningpathControllerV2 - addServlet(controller, "/*") - - override def beforeEach(): Unit = { - resetMocks() - when(languageValidator.validate(any[String], any[String], any[Boolean])) - .thenReturn(None) - } - test("That GET / will send all query-params to the search service") { val query = "hoppetau" val tag = "lek" @@ -90,31 +95,29 @@ class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with S when(searchService.matchingQuery(eqTo(expectedSettings))).thenReturn(Success(result)) - get( - "/", - Map( - "query" -> query, - "tag" -> tag, - "language" -> language, - "sort" -> "-duration", - "page-size" -> s"$pageSize", - "page" -> s"$page", - "ids" -> s"$ids", - "verificationStatus" -> s"$verificationStatus" - ) - ) { - status should equal(200) - val convertedBody = read[api.SearchResultV2](body) - convertedBody.results.head.title should equal(api.Title("Tittel", "nb")) - } + val queryParams = Map( + "query" -> query, + "tag" -> tag, + "language" -> language, + "sort" -> "-duration", + "page-size" -> s"$pageSize", + "page" -> s"$page", + "ids" -> s"$ids", + "verificationStatus" -> s"$verificationStatus" + ) + + val res = simpleHttpClient.send( + quickRequest.get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths?$queryParams") + ) + res.code.code should be(200) + val convertedBody = read[api.SearchResultV2](res.body) + convertedBody.results.head.title should equal(api.Title("Tittel", "nb")) } test("That GET / will handle all empty query-params as missing query params") { val query = "" val tag = "" val language = "" - val page = "" - val pageSize = "" val duration = "" val ids = "1,2" @@ -124,22 +127,19 @@ class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with S when(searchService.matchingQuery(any[SearchSettings])).thenReturn(Success(result)) - get( - "/", - Map( - "query" -> query, - "tag" -> tag, - "language" -> language, - "sort" -> duration, - "page-size" -> s"$pageSize", - "page" -> s"$page", - "ids" -> s"$ids" - ) - ) { - status should equal(200) - val convertedBody = read[api.SearchResultV2](body) - convertedBody.totalCount should be(-1) - } + val queryParams = Map( + "query" -> query, + "tag" -> tag, + "language" -> language, + "sort" -> duration, + "ids" -> s"$ids" + ) + val res = simpleHttpClient.send( + quickRequest.get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths?$queryParams") + ) + res.code.code should be(200) + val convertedBody = read[api.SearchResultV2](res.body) + convertedBody.totalCount should be(-1) } @@ -165,16 +165,16 @@ class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with S ) when(searchService.matchingQuery(eqTo(expectedSettings))).thenReturn(Success(result)) - - post( - "/search/", - body = - s"""{"query": "$query", "tag": "$tag", "language": "$language", "page": $page, "pageSize": $pageSize, "ids": [1, 2], "sort": "-duration" }""" - ) { - status should equal(200) - val convertedBody = read[api.SearchResultV2](body) - convertedBody.results.head.title should equal(api.Title("Tittel", "nb")) - } + val inputBody = + s"""{"query": "$query", "tag": "$tag", "language": "$language", "page": $page, "pageSize": $pageSize, "ids": [1, 2], "sort": "-duration" }""" + val res = simpleHttpClient.send( + quickRequest + .post(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/search/") + .body(inputBody) + ) + res.code.code should be(200) + val convertedBody = read[api.SearchResultV2](res.body) + convertedBody.results.head.title should equal(api.Title("Tittel", "nb")) } test("That GET /licenses with filter sat to by only returns creative common licenses") { @@ -182,17 +182,13 @@ class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with S .filter(_.license.toString.startsWith("by")) .map(l => commonApi.License(l.license.toString, Option(l.description), l.url)) .toSet - - get( - "/licenses/", - Map( - "filter" -> "by" - ) - ) { - status should equal(200) - val convertedBody = read[Set[commonApi.License]](body) - convertedBody should equal(creativeCommonlicenses) - } + val res = simpleHttpClient.send( + quickRequest + .get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/licenses/?filter=by") + ) + res.code.code should be(200) + val convertedBody = read[Set[commonApi.License]](res.body) + convertedBody should equal(creativeCommonlicenses) } test("That GET /licenses with filter not specified returns all licenses") { @@ -200,65 +196,33 @@ class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with S .map(l => commonApi.License(l.license.toString, Option(l.description), l.url)) .toSet - get("/licenses/", Map()) { - status should equal(200) - val convertedBody = read[Set[commonApi.License]](body) - convertedBody should equal(allLicenses) - } - } - - test("That paramAsListOfLong returns empty list when empty param") { - implicit val request: HttpServletRequest = mock[HttpServletRequest](withSettings.strictness(Strictness.Lenient)) - val paramName = "test" - val parameterMap = Map("someOther" -> Array("")) - - when(request.getParameterMap).thenReturn(parameterMap.asJava) - controller.paramAsListOfLong(paramName)(request) should equal(List()) - } - - test("That paramAsListOfLong returns List of longs for all ids specified in input") { - implicit val request: HttpServletRequest = mock[HttpServletRequest](withSettings.strictness(Strictness.Lenient)) - val expectedList = List(1, 2, 3, 5, 6, 7, 8) - val paramName = "test" - val parameterMap = Map(paramName -> Array(expectedList.mkString(" , "))) - - when(request.getParameterMap).thenReturn(parameterMap.asJava) - controller.paramAsListOfLong(paramName)(request) should equal(expectedList) - } - - test("That paramAsListOfLong returns validation error when list of ids contains a string") { - implicit val request: HttpServletRequest = mock[HttpServletRequest](withSettings.strictness(Strictness.Lenient)) - val paramName = "test" - val parameterMap = Map(paramName -> Array("1,2,abc,3")) - - when(request.getParameterMap).thenReturn(parameterMap.asJava) - - val validationException = intercept[ValidationException] { - controller.paramAsListOfLong(paramName)(request) - } - - validationException.errors.size should be(1) - validationException.errors.head.field should equal(paramName) - validationException.errors.head.message should equal( - s"Invalid value for $paramName. Only (list of) digits are allowed." + val res = simpleHttpClient.send( + quickRequest + .get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/licenses/") ) - + res.code.code should be(200) + val convertedBody = read[Set[commonApi.License]](res.body) + convertedBody should equal(allLicenses) } test("That /with-status returns 400 if invalid status is specified") { when(readService.learningPathWithStatus(any[String], any[TokenUser])) .thenReturn(Failure(InvalidStatusException("Bad status"))) - get("/status/invalidStatusHurrDurr") { - status should equal(400) - } + val res = simpleHttpClient.send( + quickRequest + .get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/status/invalidStatusHurrDurr") + ) + res.code.code should be(400) when(readService.learningPathWithStatus(any[String], any[TokenUser])) .thenReturn(Success(List.empty)) - get("/status/unlisted") { - status should equal(200) - } + val res2 = simpleHttpClient.send( + quickRequest + .get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/status/unlisted") + ) + res2.code.code should be(200) } test("That scrollId is in header, and not in body") { @@ -274,11 +238,12 @@ class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with S ) when(searchService.matchingQuery(any[SearchSettings])).thenReturn(Success(searchResponse)) - get(s"/") { - status should be(200) - body.contains(scrollId) should be(false) - response.getHeader("search-context") should be(scrollId) - } + val res = simpleHttpClient.send( + quickRequest.get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/") + ) + res.code.code should be(200) + res.body.contains(scrollId) should be(false) + res.header("search-context") should be(Some(scrollId)) } test("That scrolling uses scroll and not searches normally") { @@ -296,9 +261,10 @@ class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with S when(searchService.scroll(anyString, anyString)).thenReturn(Success(searchResponse)) - get(s"/?search-context=$scrollId") { - status should be(200) - } + val res = simpleHttpClient.send( + quickRequest.get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/?search-context=$scrollId") + ) + res.code.code should be(200) verify(searchService, times(0)).matchingQuery(any[SearchSettings]) verify(searchService, times(1)).scroll(eqTo(scrollId), any[String]) @@ -319,9 +285,12 @@ class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with S when(searchService.scroll(anyString, anyString)).thenReturn(Success(searchResponse)) - post(s"/search/", body = s"""{"scrollId":"$scrollId"}""") { - status should be(200) - } + val res = simpleHttpClient.send( + quickRequest + .post(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/search/") + .body(s"""{"scrollId":"$scrollId"}""") + ) + res.code.code should be(200) verify(searchService, times(0)).matchingQuery(any[SearchSettings]) verify(searchService, times(1)).scroll(eqTo(scrollId), any[String]) @@ -346,11 +315,12 @@ class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with S ) when(searchService.matchingQuery(any[SearchSettings])).thenReturn(Success(result)) - get("/?search-context=initial") { - status should be(200) - verify(searchService, times(1)).matchingQuery(expectedSettings) - verify(searchService, times(0)).scroll(any[String], any[String]) - } + val res = simpleHttpClient.send( + quickRequest.get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/?search-context=initial") + ) + res.code.code should be(200) + verify(searchService, times(1)).matchingQuery(expectedSettings) + verify(searchService, times(0)).scroll(any[String], any[String]) } test("That GET /contains-article returns 200") { @@ -367,20 +337,33 @@ class LearningpathControllerV2Test extends UnitSuite with TestEnvironment with S when(taxonomyApiClient.queryNodes(any[Long])).thenReturn(Success(List[Node]())) when(searchService.containsPath(any[List[String]])).thenReturn(Success(result)) - get("/contains-article/123") { - status should be(200) - } + val res = simpleHttpClient.send( + quickRequest.get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/contains-article/123") + ) + res.code.code should be(200) } test("That GET /contains-article returns correct errors when id is a string or nothing") { reset(taxonomyApiClient) + when(taxonomyApiClient.queryNodes(any[Long])).thenReturn(Success(List[Node]())) - get("/contains-article/hallohallo") { - status should be(400) - } + val res = simpleHttpClient.send( + quickRequest.get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/contains-article/hallohallo") + ) + res.code.code should be(400) + + val res2 = simpleHttpClient.send( + quickRequest.get(uri"http://localhost:$serverPort/learningpath-api/v2/learningpaths/contains-article/") + ) + res2.code.code should be(400) + } - get("/contains-article/") { - status should be(404) + test("That no endpoints are shadowed") { + import sttp.tapir.testing.EndpointVerifier + val errors = EndpointVerifier(controller.endpoints.map(_.endpoint)) + if (errors.nonEmpty) { + val errString = errors.map(e => e.toString).mkString("\n\t- ", "\n\t- ", "") + fail(s"Got errors when verifying ${controller.serviceName} controller:$errString") } } } diff --git a/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/StatsControllerTest.scala b/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/StatsControllerTest.scala index 49f497735e..cfa4e64648 100644 --- a/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/StatsControllerTest.scala +++ b/learningpath-api/src/test/scala/no/ndla/learningpathapi/controller/StatsControllerTest.scala @@ -7,26 +7,45 @@ package no.ndla.learningpathapi.controller -import no.ndla.learningpathapi.{TestEnvironment, UnitSuite} +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import no.ndla.learningpathapi.{Eff, TestEnvironment, UnitSuite} +import no.ndla.network.tapir.Service import org.json4s.DefaultFormats -import org.scalatra.test.scalatest.ScalatraFunSuite +import sttp.client3.quick._ -class StatsControllerTest extends UnitSuite with TestEnvironment with ScalatraFunSuite { +class StatsControllerTest extends UnitSuite with TestEnvironment { implicit val formats: DefaultFormats.type = org.json4s.DefaultFormats - implicit val swagger: LearningpathSwagger = new LearningpathSwagger + val serverPort: Int = findFreePort + val controller = new StatsController + override val services: List[Service[Eff]] = List(controller) - lazy val controller = new StatsController() - addServlet(controller, "/*") + override def beforeAll(): Unit = { + IO { Routes.startJdkServer("StatsControllerTest", serverPort) {} }.unsafeRunAndForget() + Thread.sleep(1000) + } override def beforeEach(): Unit = { resetMocks() } test("That getting stats redirects to the correct endpoint") { - get("/") { - status should be(301) - response.getHeader("Location") should be("/myndla-api/v1/stats") + val res = simpleHttpClient.send( + quickRequest + .get(uri"http://localhost:$serverPort/learningpath-api/v1/stats") + .followRedirects(false) + ) + res.header("Location") should be(Some("/myndla-api/v1/stats")) + res.code.code should be(301) + } + + test("That no endpoints are shadowed") { + import sttp.tapir.testing.EndpointVerifier + val errors = EndpointVerifier(controller.endpoints.map(_.endpoint)) + if (errors.nonEmpty) { + val errString = errors.map(e => e.toString).mkString("\n\t- ", "\n\t- ", "") + fail(s"Got errors when verifying ${controller.serviceName} controller:$errString") } } diff --git a/myndla/src/main/scala/no/ndla/myndla/model/api/Breadcrumb.scala b/myndla/src/main/scala/no/ndla/myndla/model/api/Breadcrumb.scala index f8da0dea2f..ab8b041c49 100644 --- a/myndla/src/main/scala/no/ndla/myndla/model/api/Breadcrumb.scala +++ b/myndla/src/main/scala/no/ndla/myndla/model/api/Breadcrumb.scala @@ -9,13 +9,11 @@ package no.ndla.myndla.model.api import io.circe.generic.semiauto._ import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description case class Breadcrumb( - @(ApiModelProperty @field)(description = "UUID of the folder") id: String, - @(ApiModelProperty @field)(description = "Folder name") name: String + @description("UUID of the folder") id: String, + @description("Folder name") name: String ) object Breadcrumb { diff --git a/myndla/src/main/scala/no/ndla/myndla/model/api/ExportedUserData.scala b/myndla/src/main/scala/no/ndla/myndla/model/api/ExportedUserData.scala index a46647ee5b..5fde3fbfbe 100644 --- a/myndla/src/main/scala/no/ndla/myndla/model/api/ExportedUserData.scala +++ b/myndla/src/main/scala/no/ndla/myndla/model/api/ExportedUserData.scala @@ -6,15 +6,13 @@ */ package no.ndla.myndla.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import org.scalatra.swagger.annotations.ApiModelProperty - -import scala.annotation.meta.field +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description case class ExportedUserData( - @(ApiModelProperty @field)(description = "The users data") userData: MyNDLAUser, - @(ApiModelProperty @field)(description = "The users folders") folders: List[Folder] + @description("The users data") userData: MyNDLAUser, + @description("The users folders") folders: List[Folder] ) object ExportedUserData { diff --git a/myndla/src/main/scala/no/ndla/myndla/model/api/Folder.scala b/myndla/src/main/scala/no/ndla/myndla/model/api/Folder.scala index e3db2741ce..dd3c78a7b0 100644 --- a/myndla/src/main/scala/no/ndla/myndla/model/api/Folder.scala +++ b/myndla/src/main/scala/no/ndla/myndla/model/api/Folder.scala @@ -14,13 +14,12 @@ import io.circe.syntax.EncoderOps import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate import no.ndla.myndla.model.domain.{CopyableFolder, CopyableResource, ResourceType} -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field import scala.annotation.unused case class Owner( - @(ApiModelProperty @field)(description = "Name of the owner") name: String + @description("Name of the owner") name: String ) object Owner { @@ -30,19 +29,19 @@ object Owner { // format: off case class Folder( - @(ApiModelProperty @field)(description = "UUID of the folder") id: String, - @(ApiModelProperty @field)(description = "Folder name") name: String, - @(ApiModelProperty @field)(description = "Folder status") status: String, - @(ApiModelProperty @field)(description = "UUID of parent folder") parentId: Option[String], - @(ApiModelProperty @field)(description = "List of parent folders to resource") breadcrumbs: List[Breadcrumb], - @(ApiModelProperty @field)(description = "List of subfolders") subfolders: List[FolderData], - @(ApiModelProperty @field)(description = "List of resources") resources: List[Resource], - @(ApiModelProperty @field)(description = "Where the folder is sorted within its parent") rank: Option[Int], - @(ApiModelProperty @field)(description = "When the folder was created") created: NDLADate, - @(ApiModelProperty @field)(description = "When the folder was updated") updated: NDLADate, - @(ApiModelProperty @field)(description = "When the folder was last shared") shared: Option[NDLADate], - @(ApiModelProperty @field)(description = "Description of the folder") description: Option[String], - @(ApiModelProperty @field)(description = "Owner of the folder, if the owner have opted in to share their name") owner: Option[Owner], + @description("UUID of the folder") id: String, + @description("Folder name") name: String, + @description("Folder status") status: String, + @description("UUID of parent folder") parentId: Option[String], + @description("List of parent folders to resource") breadcrumbs: List[Breadcrumb], + @description("List of subfolders") subfolders: List[FolderData], + @description("List of resources") resources: List[Resource], + @description("Where the folder is sorted within its parent") rank: Option[Int], + @description("When the folder was created") created: NDLADate, + @description("When the folder was updated") updated: NDLADate, + @description("When the folder was last shared") shared: Option[NDLADate], + @description("Description of the folder") description: Option[String], + @description("Owner of the folder, if the owner have opted in to share their name") owner: Option[Owner], ) extends FolderData with CopyableFolder // format: on @@ -108,27 +107,27 @@ object FolderData { } case class NewFolder( - @(ApiModelProperty @field)(description = "Folder name") name: String, - @(ApiModelProperty @field)(description = "Id of parent folder") parentId: Option[String], - @(ApiModelProperty @field)(description = "Status of the folder (private, shared)") status: Option[String], - @(ApiModelProperty @field)(description = "Description of the folder") description: Option[String] + @description("Folder name") name: String, + @description("Id of parent folder") parentId: Option[String], + @description("Status of the folder (private, shared)") status: Option[String], + @description("Description of the folder") description: Option[String] ) case class UpdatedFolder( - @(ApiModelProperty @field)(description = "Folder name") name: Option[String], - @(ApiModelProperty @field)(description = "Status of the folder (private, shared)") status: Option[String], - @(ApiModelProperty @field)(description = "Description of the folder") description: Option[String] + @description("Folder name") name: Option[String], + @description("Status of the folder (private, shared)") status: Option[String], + @description("Description of the folder") description: Option[String] ) // format: off case class Resource( - @(ApiModelProperty @field)(description = "Unique ID of the resource") id: String, - @(ApiModelProperty @field)(description = "Type of the resource. (Article, Learningpath)") resourceType: ResourceType, - @(ApiModelProperty @field)(description = "Relative path of this resource") path: String, - @(ApiModelProperty @field)(description = "When the resource was created") created: NDLADate, - @(ApiModelProperty @field)(description = "List of tags") tags: List[String], - @(ApiModelProperty @field)(description = "The id of the resource, useful for fetching metadata for the resource") resourceId: String, - @(ApiModelProperty @field)(description = "The which rank the resource appears in a sorted sequence") rank: Option[Int] + @description("Unique ID of the resource") id: String, + @description("Type of the resource. (Article, Learningpath)") resourceType: ResourceType, + @description("Relative path of this resource") path: String, + @description("When the resource was created") created: NDLADate, + @description("List of tags") tags: List[String], + @description("The id of the resource, useful for fetching metadata for the resource") resourceId: String, + @description("The which rank the resource appears in a sorted sequence") rank: Option[Int] ) extends CopyableResource // format: on @@ -138,19 +137,13 @@ object Resource { } case class NewResource( - @(ApiModelProperty @field)( - description = "Type of the resource. (Article, Learningpath)" - ) resourceType: ResourceType, - @(ApiModelProperty @field)(description = "Relative path of this resource") path: String, - @(ApiModelProperty @field)(description = "List of tags") tags: Option[List[String]], - @(ApiModelProperty @field)( - description = "The id of the resource, useful for fetching metadata for the resource" - ) resourceId: String + @description("Type of the resource. (Article, Learningpath)") resourceType: ResourceType, + @description("Relative path of this resource") path: String, + @description("List of tags") tags: Option[List[String]], + @description("The id of the resource, useful for fetching metadata for the resource") resourceId: String ) case class UpdatedResource( - @(ApiModelProperty @field)(description = "List of tags") tags: Option[List[String]], - @(ApiModelProperty @field)( - description = "The id of the resource, useful for fetching metadata for the resource" - ) resourceId: Option[String] + @description("List of tags") tags: Option[List[String]], + @description("The id of the resource, useful for fetching metadata for the resource") resourceId: Option[String] ) diff --git a/myndla/src/main/scala/no/ndla/myndla/model/api/FolderSortRequest.scala b/myndla/src/main/scala/no/ndla/myndla/model/api/FolderSortRequest.scala index 7ca6f7b565..eb622b22a2 100644 --- a/myndla/src/main/scala/no/ndla/myndla/model/api/FolderSortRequest.scala +++ b/myndla/src/main/scala/no/ndla/myndla/model/api/FolderSortRequest.scala @@ -7,13 +7,10 @@ package no.ndla.myndla.model.api -import org.scalatra.swagger.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description import java.util.UUID -import scala.annotation.meta.field case class FolderSortRequest( - @(ApiModelProperty @field)( - description = "List of the children ids in sorted order, MUST be all ids" - ) sortedIds: Seq[UUID] + @description("List of the children ids in sorted order, MUST be all ids") sortedIds: Seq[UUID] ) diff --git a/myndla/src/main/scala/no/ndla/myndla/model/api/MyNDLAUser.scala b/myndla/src/main/scala/no/ndla/myndla/model/api/MyNDLAUser.scala index 4f5abffe68..acd47b9ef7 100644 --- a/myndla/src/main/scala/no/ndla/myndla/model/api/MyNDLAUser.scala +++ b/myndla/src/main/scala/no/ndla/myndla/model/api/MyNDLAUser.scala @@ -7,18 +7,16 @@ package no.ndla.myndla.model.api -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.myndla.model.domain.ArenaGroup -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description case class MyNDLAGroup( - @(ApiModelProperty @field)(description = "ID of the group") id: String, - @(ApiModelProperty @field)(description = "Name of the group") displayName: String, - @(ApiModelProperty @field)(description = "Is this the primary school") isPrimarySchool: Boolean, - @(ApiModelProperty @field)(description = "ID of parent group") parentId: Option[String] + @description("ID of the group") id: String, + @description("Name of the group") displayName: String, + @description("Is this the primary school") isPrimarySchool: Boolean, + @description("ID of parent group") parentId: Option[String] ) object MyNDLAGroup { @@ -27,18 +25,18 @@ object MyNDLAGroup { } case class MyNDLAUser( - @ApiModelProperty(description = "ID of the user") id: Long, - @ApiModelProperty(description = "FeideID of the user") feideId: String, - @ApiModelProperty(description = "Username of the user") username: String, - @ApiModelProperty(description = "Email address of the user") email: String, - @ApiModelProperty(description = "Name of the user") displayName: String, - @ApiModelProperty(description = "Favorite subjects of the user") favoriteSubjects: Seq[String], - @ApiModelProperty(description = "User role") role: String, - @ApiModelProperty(description = "User root organization") organization: String, - @ApiModelProperty(description = "User groups") groups: Seq[MyNDLAGroup], - @ApiModelProperty(description = "Whether arena is explicitly enabled for the user") arenaEnabled: Boolean, - @ApiModelProperty(description = "Whether users name is shared with folders or not") shareName: Boolean, - @ApiModelProperty(description = "Arena user groups") arenaGroups: List[ArenaGroup] + @description("ID of the user") id: Long, + @description("FeideID of the user") feideId: String, + @description("Username of the user") username: String, + @description("Email address of the user") email: String, + @description("Name of the user") displayName: String, + @description("Favorite subjects of the user") favoriteSubjects: Seq[String], + @description("User role") role: String, + @description("User root organization") organization: String, + @description("User groups") groups: Seq[MyNDLAGroup], + @description("Whether arena is explicitly enabled for the user") arenaEnabled: Boolean, + @description("Whether users name is shared with folders or not") shareName: Boolean, + @description("Arena user groups") arenaGroups: List[ArenaGroup] ) object MyNDLAUser { @@ -48,10 +46,10 @@ object MyNDLAUser { // format: off case class UpdatedMyNDLAUser( - @(ApiModelProperty @field)(description = "Favorite subjects of the user") favoriteSubjects: Option[Seq[String]], - @(ApiModelProperty @field)(description = "Whether arena should explicitly be enabled for the user") arenaEnabled: Option[Boolean], - @(ApiModelProperty @field)(description = "Whether users name should be shared with folder or not") shareName: Option[Boolean], - @(ApiModelProperty @field)(description = "Which arena groups the user should be in, only modifiable by admins") arenaGroups: Option[List[ArenaGroup]] + @description("Favorite subjects of the user") favoriteSubjects: Option[Seq[String]], + @description("Whether arena should explicitly be enabled for the user") arenaEnabled: Option[Boolean], + @description("Whether users name should be shared with folder or not") shareName: Option[Boolean], + @description("Which arena groups the user should be in, only modifiable by admins") arenaGroups: Option[List[ArenaGroup]] ) object UpdatedMyNDLAUser { diff --git a/myndla/src/main/scala/no/ndla/myndla/model/api/Stats.scala b/myndla/src/main/scala/no/ndla/myndla/model/api/Stats.scala index abb11d7f3a..24c222ec8b 100644 --- a/myndla/src/main/scala/no/ndla/myndla/model/api/Stats.scala +++ b/myndla/src/main/scala/no/ndla/myndla/model/api/Stats.scala @@ -9,20 +9,17 @@ package no.ndla.myndla.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.ApiModel -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Stats for my-ndla usage") +@description("Stats for my-ndla usage") case class Stats( - @(ApiModelProperty @field)(description = "The total number of users registered ") numberOfUsers: Long, - @(ApiModelProperty @field)(description = "The total number of created folders") numberOfFolders: Long, - @(ApiModelProperty @field)(description = "The total number of favourited resources ") numberOfResources: Long, - @(ApiModelProperty @field)(description = "The total number of created tags") numberOfTags: Long, - @(ApiModelProperty @field)(description = "The total number of favourited subjects") numberOfSubjects: Long, - @(ApiModelProperty @field)(description = "The total number of shared folders") numberOfSharedFolders: Long, - @(ApiModelProperty @field)(description = "Stats for type resources") favouritedResources: List[ResourceStats] + @description("The total number of users registered ") numberOfUsers: Long, + @description("The total number of created folders") numberOfFolders: Long, + @description("The total number of favourited resources ") numberOfResources: Long, + @description("The total number of created tags") numberOfTags: Long, + @description("The total number of favourited subjects") numberOfSubjects: Long, + @description("The total number of shared folders") numberOfSharedFolders: Long, + @description("Stats for type resources") favouritedResources: List[ResourceStats] ) object Stats { @@ -31,8 +28,8 @@ object Stats { } case class ResourceStats( - @(ApiModelProperty @field)(description = "The type of favourited resouce") `type`: String, - @(ApiModelProperty @field)(description = "The number of favourited resource") number: Long + @description("The type of favourited resouce") `type`: String, + @description("The number of favourited resource") number: Long ) object ResourceStats { diff --git a/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMeta.scala b/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMeta.scala index c218ff80e8..3d5a624989 100644 --- a/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMeta.scala +++ b/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMeta.scala @@ -7,18 +7,17 @@ package no.ndla.myndla.model.api.config -import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.ApiModelProperty -import org.scalatra.swagger.runtime.annotations.ApiModel import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate +import sttp.tapir.Schema.annotations.description -@ApiModel(description = "Describes configuration value.") +@description("Describes configuration value.") case class ConfigMeta( - @ApiModelProperty(description = "Configuration key") key: String, - @ApiModelProperty(description = "Configuration value.") value: Either[Boolean, List[String]], - @ApiModelProperty(description = "Date of when configuration was last updated") updatedAt: NDLADate, - @ApiModelProperty(description = "UserId of who last updated the configuration parameter.") updatedBy: String + @description("Configuration key") key: String, + @description("Configuration value.") value: Either[Boolean, List[String]], + @description("Date of when configuration was last updated") updatedAt: NDLADate, + @description("UserId of who last updated the configuration parameter.") updatedBy: String ) object ConfigMeta { diff --git a/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMetaRestricted.scala b/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMetaRestricted.scala index bfb5a6d855..531a48f0fb 100644 --- a/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMetaRestricted.scala +++ b/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMetaRestricted.scala @@ -7,17 +7,14 @@ package no.ndla.myndla.model.api.config -import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.ApiModelProperty -import org.scalatra.swagger.runtime.annotations.ApiModel import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Describes configuration value.") +@description("Describes configuration value.") case class ConfigMetaRestricted( - @(ApiModelProperty @field)(description = "Configuration key") key: String, - @(ApiModelProperty @field)(description = "Configuration value.") value: Either[Boolean, List[String]] + @description("Configuration key") key: String, + @description("Configuration value.") value: Either[Boolean, List[String]] ) object ConfigMetaRestricted { diff --git a/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMetaValue.scala b/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMetaValue.scala index 208ab65713..04ae70e956 100644 --- a/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMetaValue.scala +++ b/myndla/src/main/scala/no/ndla/myndla/model/api/config/ConfigMetaValue.scala @@ -11,12 +11,10 @@ import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} import no.ndla.myndla.model.domain.config import no.ndla.myndla.model.domain.config.{BooleanValue, StringListValue} -import org.scalatra.swagger.annotations.ApiModelProperty - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description case class ConfigMetaValue( - @(ApiModelProperty @field)(description = "Value to set configuration param to.") + @description("Value to set configuration param to.") value: Either[Boolean, List[String]] ) diff --git a/network/src/main/scala/no/ndla/network/scalatra/BaseHealthController.scala b/network/src/main/scala/no/ndla/network/scalatra/BaseHealthController.scala deleted file mode 100644 index 6b51464fee..0000000000 --- a/network/src/main/scala/no/ndla/network/scalatra/BaseHealthController.scala +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Part of NDLA network - * Copyright (C) 2022 NDLA - * - * See LICENSE - * - */ -package no.ndla.network.scalatra - -import no.ndla.common.Warmup -import no.ndla.network.model.RequestInfo -import org.scalatra._ - -class BaseHealthController extends ScalatraServlet with Warmup { - - before() { - RequestInfo.fromRequest(request).setThreadContextRequestInfo() - } - - protected def doIfWarmedUp(func: => Any): Any = { - if (isWarmedUp) { - func - } else { - InternalServerError("Warmup not finished") - } - } - - override def get(transformers: RouteTransformer*)(action: => Any): Route = - addRoute( - Get, - transformers, - doIfWarmedUp(action) - ) - - get("/") { - Ok() - }: Unit -} diff --git a/network/src/main/scala/no/ndla/network/scalatra/NdlaControllerBase.scala b/network/src/main/scala/no/ndla/network/scalatra/NdlaControllerBase.scala deleted file mode 100644 index 2704d699c2..0000000000 --- a/network/src/main/scala/no/ndla/network/scalatra/NdlaControllerBase.scala +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Part of NDLA network. - * Copyright (C) 2022 NDLA - * - * See LICENSE - * - */ - -package no.ndla.network.scalatra - -import com.typesafe.scalalogging.StrictLogging -import no.ndla.common.RequestLogger.beforeRequestLogString -import no.ndla.common.configuration.HasBaseProps -import no.ndla.common.errors.{AccessDeniedException, ValidationException, ValidationMessage} -import no.ndla.network.model.RequestInfo -import no.ndla.network.tapir.auth.{Permission, TokenUser} -import org.json4s.ext.JavaTimeSerializers -import org.json4s.native.Serialization.read -import org.json4s.{DefaultFormats, Formats} -import org.scalatra.json.NativeJsonSupport -import org.scalatra._ -import cats.implicits._ -import no.ndla.common.model.NDLADate -import no.ndla.common.model.NDLADate.asString - -import javax.servlet.http.HttpServletRequest -import scala.util.{Failure, Success, Try} - -trait NdlaControllerBase { - this: HasBaseProps => - - trait NdlaControllerBase extends ScalatraServlet with NativeJsonSupport with StrictLogging { - protected implicit override val jsonFormats: Formats = - DefaultFormats ++ - JavaTimeSerializers.all + - NDLADate.Json4sSerializer - - def requirePermissionOrAccessDenied( - requiredPermission: Permission, - isAllowedWithoutPermission: Boolean = false - )(f: => Any): Any = - if (isAllowedWithoutPermission) { f } - else { requirePermissionOrAccessDenied(requiredPermission)(_ => f) } - - def requirePermissionOrAccessDeniedWithUser(requiredPermission: Permission)(f: TokenUser => Any): Any = - requirePermissionOrAccessDenied(requiredPermission)(f) - - def requireUserId(f: TokenUser => Any): Any = doIfAccessTrue(_.jwt.ndla_id.isDefined)(f) - - def doIfAccessTrue(checkAccess: TokenUser => Boolean)(f: TokenUser => Any): Any = - TokenUser.fromScalatraRequest(request) match { - case Success(user) if checkAccess(user) => f(user) - case Success(_) => errorHandler(AccessDeniedException.forbidden) - case Failure(_) => errorHandler(AccessDeniedException.unauthorized) - } - - def doWithUser(f: Option[TokenUser] => Any): Any = { - TokenUser.fromScalatraRequest(request) match { - case Failure(_) => f(None) - case Success(user) => f(Some(user)) - } - } - - private def requirePermissionOrAccessDenied(requiredPermission: Permission)(f: TokenUser => Any): Any = - doIfAccessTrue { user => - user.hasPermission(requiredPermission) - }(f) - - type NdlaErrorHandler = PartialFunction[Throwable, ActionResult] - def ndlaErrorHandler: NdlaErrorHandler - - before() { - RequestInfo.fromRequest(request).setThreadContextRequestInfo() - - logger.info( - beforeRequestLogString(request.getMethod, request.getRequestURI, request.queryString) - ) - } - - // See: no.ndla.common.scalatra.NdlaRequestLogger for whats logged on `after`. - - error { ndlaErrorHandler } - - /** Custom renderer to allow for returning Try[T] from controller endpoints. Where `Failure(ex)` will pass `ex` to - * `ndlaErrorHandler` to be handled like a thrown exception. And `Success(value)` will return `Ok(value)` from the - * endpoint. - */ - private val tryRenderer: RenderPipeline = { - case Success(value) => Ok(value) - case Failure(ex) => - Try(ndlaErrorHandler(ex)) match { - case Failure(ex: HaltException) => renderHaltException(ex) - case Failure(ex) => errorHandler(ex) - case Success(result) => result - } - } - override def renderPipeline: RenderPipeline = tryRenderer.orElse(super.renderPipeline) - - private def isInteger(value: String): Boolean = value.forall(_.isDigit) - private def isDouble(value: String): Boolean = Try(value.toDouble).isSuccess - private def isBoolean(value: String): Boolean = Try(value.toBoolean).isSuccess - - def long(paramName: String)(implicit request: HttpServletRequest): Long = { - val paramValue = params(paramName) - if (!isInteger(paramValue)) - throw ValidationException( - "Validation Error", - Seq(ValidationMessage(paramName, s"Invalid value for $paramName. Only digits are allowed.")) - ) - - paramValue.toLong - } - - def int(paramName: String)(implicit request: HttpServletRequest): Try[Int] = { - val paramValue = params(paramName) - if (!isInteger(paramValue)) - Failure( - ValidationException( - "Validation Error", - Seq(ValidationMessage(paramName, s"Invalid value for $paramName. Only digits are allowed.")) - ) - ) - else Try(paramValue.toInt) - } - - def extractDoubleOpt2(one: String, two: String)(implicit - request: HttpServletRequest - ): (Option[Double], Option[Double]) = { - (extractDoubleOpt(one), extractDoubleOpt(two)) - } - - private def extractDoubleOpt(paramName: String)(implicit request: HttpServletRequest): Option[Double] = { - params.get(paramName) match { - case Some(value) => - if (!isDouble(value)) - throw ValidationException( - errors = Seq(ValidationMessage(paramName, s"Invalid value for $paramName. Only numbers are allowed.")) - ) - - Some(value.toDouble) - case _ => None - } - } - - def extractDoubleOpts(paramNames: String*)(implicit request: HttpServletRequest): Seq[Option[Double]] = { - paramNames.map(paramName => { - params.get(paramName) match { - case Some(value) => - if (!isDouble(value)) - throw ValidationException( - errors = Seq(ValidationMessage(paramName, s"Invalid value for $paramName. Only numbers are allowed.")) - ) - - Some(value.toDouble) - case _ => None - } - }) - } - - def booleanOrDefault(paramName: String, default: Boolean)(implicit request: HttpServletRequest): Boolean = { - val paramValue = paramOrDefault(paramName, "") - if (!isBoolean(paramValue)) default else paramValue.toBoolean - } - - def booleanOrNone(paramName: String)(implicit request: HttpServletRequest): Option[Boolean] = { - params.get(paramName).map(_.trim).filterNot(_.isEmpty).flatMap(_.toBooleanOption) - } - - def paramOrNone(paramName: String)(implicit request: HttpServletRequest): Option[String] = { - params.get(paramName).map(_.trim).filterNot(_.isEmpty()) - } - - def doubleOrNone(name: String)(implicit request: HttpServletRequest): Option[Double] = { - paramOrNone(name).flatMap(i => Try(i.toDouble).toOption) - } - - def intOrNone(name: String)(implicit request: HttpServletRequest): Option[Int] = { - paramOrNone(name).flatMap(i => Try(i.toInt).toOption) - } - - def castIntOrNone(name: String)(implicit request: HttpServletRequest): Option[Int] = { - doubleOrNone(name).map(_.toInt) - } - - def paramOrDefault(paramName: String, default: String)(implicit request: HttpServletRequest): String = { - paramOrNone(paramName).getOrElse(default) - } - - def paramAsListOfString(paramName: String)(implicit request: HttpServletRequest): List[String] = { - params.get(paramName).filter(_.nonEmpty) match { - case None => List.empty - case Some(param) => param.split(",").toList.map(_.trim) - } - } - - def intOrDefault(paramName: String, default: Int)(implicit request: HttpServletRequest): Int = - intOrNone(paramName).getOrElse(default) - - def doubleInRange(paramName: String, from: Int, to: Int)(implicit request: HttpServletRequest): Option[Double] = { - doubleOrNone(paramName) match { - case Some(d) if d >= Math.min(from, to) && d <= Math.max(from, to) => Some(d) - case Some(d) => - throw ValidationException( - errors = Seq( - ValidationMessage(paramName, s"Invalid value for $paramName. Must be in range $from-$to but was $d") - ) - ) - case None => None - } - } - - private val digitsOnlyError = (paramName: String) => - Failure( - new ValidationException( - errors = Seq(ValidationMessage(paramName, s"Invalid value for $paramName. Only digits are allowed.")) - ) - ) - - def stringParamToLong(paramName: String, paramValue: String): Try[Long] = { - paramValue.forall(_.isDigit) match { - case true => Try(paramValue.toLong).recoverWith(_ => digitsOnlyError(paramName)) - case false => digitsOnlyError(paramName) - } - } - - def paramAsListOfLong(paramName: String)(implicit request: HttpServletRequest): List[Long] = { - val strings = paramAsListOfString(paramName) - strings.headOption match { - case None => List.empty - case Some(_) => - if (!strings.forall(entry => entry.forall(_.isDigit))) { - throw ValidationException(paramName, s"Invalid value for $paramName. Only (list of) digits are allowed.") - } - strings.map(_.toLong) - } - } - - def longOrNone(paramName: String)(implicit request: HttpServletRequest): Option[Long] = - paramOrNone(paramName).flatMap(p => Try(p.toLong).toOption) - - def paramAsDateOrNone(paramName: String)(implicit request: HttpServletRequest): Option[NDLADate] = { - paramOrNone(paramName).map(dateString => { - NDLADate.fromString(dateString) match { - case Success(date) => date - case Failure(_) => - throw new ValidationException( - errors = Seq( - ValidationMessage( - paramName, - s"Invalid date passed. Expected format is \"${asString(NDLADate.now())}\"" - ) - ) - ) - } - }) - } - - def tryExtract[T](json: String)(implicit formats: Formats, mf: scala.reflect.Manifest[T]): Try[T] = { - Try(read[T](json)(formats, mf)) - .recoverWith(e => Failure(ValidationException(errors = Seq(ValidationMessage("body", e.getMessage))))) - } - - def extract[T](json: String)(implicit formats: Formats, mf: scala.reflect.Manifest[T]): T = { - tryExtract[T](json)(formats, mf) match { - case Success(data) => data - case Failure(e) => - logger.error(e.getMessage, e) - throw e - } - } - } -} diff --git a/network/src/main/scala/no/ndla/network/scalatra/NdlaRequestLogger.scala b/network/src/main/scala/no/ndla/network/scalatra/NdlaRequestLogger.scala deleted file mode 100644 index 9356a90505..0000000000 --- a/network/src/main/scala/no/ndla/network/scalatra/NdlaRequestLogger.scala +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Part of NDLA network. - * Copyright (C) 2022 NDLA - * - * See LICENSE - * - */ - -package no.ndla.network.scalatra - -import com.typesafe.scalalogging.StrictLogging -import no.ndla.common.RequestLogger.afterRequestLogString -import no.ndla.network.model.RequestInfo -import org.eclipse.jetty.server.{Request, RequestLog, Response} - -class NdlaRequestLogger() extends RequestLog with StrictLogging { - - override def log(request: Request, response: Response): Unit = { - if (request.getRequestURI == "/health") return // Logging health-endpoints are very noisy - - val latency = System.currentTimeMillis() - request.getTimeStamp - val query = Option(request.getQueryString).getOrElse("") - logger.info( - afterRequestLogString( - method = request.getMethod, - requestPath = request.getRequestURI, - queryString = query, - latency = latency, - responseCode = response.getStatus - ) - ) - - RequestInfo.clear() - } -} diff --git a/network/src/main/scala/no/ndla/network/scalatra/NdlaScalatraBootstrapBase.scala b/network/src/main/scala/no/ndla/network/scalatra/NdlaScalatraBootstrapBase.scala deleted file mode 100644 index ac1bb15ddb..0000000000 --- a/network/src/main/scala/no/ndla/network/scalatra/NdlaScalatraBootstrapBase.scala +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Part of NDLA network. - * Copyright (C) 2022 NDLA - * - * See LICENSE - * - */ - -package no.ndla.network.scalatra - -import org.scalatra.LifeCycle -import javax.servlet.ServletContext - -abstract class NdlaScalatraBootstrapBase[T] extends LifeCycle { - override def init(context: ServletContext): Unit = { - val componentRegistry = context.getAttribute("ComponentRegistry").asInstanceOf[T] - ndlaInit(context, componentRegistry) - } - - def ndlaInit(context: ServletContext, componentRegistry: T): Unit -} diff --git a/network/src/main/scala/no/ndla/network/scalatra/NdlaScalatraServer.scala b/network/src/main/scala/no/ndla/network/scalatra/NdlaScalatraServer.scala deleted file mode 100644 index 6038443ec1..0000000000 --- a/network/src/main/scala/no/ndla/network/scalatra/NdlaScalatraServer.scala +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Part of NDLA network. - * Copyright (C) 2022 NDLA - * - * See LICENSE - * - */ - -package no.ndla.network.scalatra - -import com.typesafe.scalalogging.StrictLogging -import net.bull.javamelody.{MonitoringFilter, Parameter, ReportServlet, SessionListener} -import no.ndla.common.Warmup -import no.ndla.common.configuration.{BaseComponentRegistry, BaseProps} -import org.eclipse.jetty.server.Server -import org.eclipse.jetty.servlet.{DefaultServlet, FilterHolder, ServletContextHandler} -import org.scalatra.servlet.ScalatraListener -import java.util -import javax.servlet.DispatcherType -import scala.io.Source - -class NdlaScalatraServer[PropType <: BaseProps, CR <: BaseComponentRegistry[PropType]]( - bootstrapPackage: String, - componentRegistry: CR, - afterHeaderBeforeStart: => Unit, - warmupFunction: ((String, Map[String, String]) => Unit) => Unit -) extends Server(componentRegistry.props.ApplicationPort) - with StrictLogging { - - val props: PropType = componentRegistry.props - private val startMillis: Long = System.currentTimeMillis() - - private def setupJavaMelody(context: ServletContextHandler): Unit = { - context.addServlet(classOf[ReportServlet], "/monitoring") - context.addEventListener(new SessionListener) - val monitoringFilter = new FilterHolder(new MonitoringFilter()) - monitoringFilter.setInitParameter(Parameter.APPLICATION_NAME.getCode, props.ApplicationName) - context.addFilter(monitoringFilter, "/*", util.EnumSet.of(DispatcherType.REQUEST, DispatcherType.ASYNC)) - } - - private def setupServletContext(): Unit = { - val context = new ServletContextHandler() - context setContextPath "/" - context.setInitParameter("org.scalatra.LifeCycle", bootstrapPackage) - context.addEventListener(new ScalatraListener) - context.addServlet(classOf[DefaultServlet], "/") - context.setInitParameter("org.eclipse.jetty.servlet.Default.dirAllowed", "false") - - setupJavaMelody(context) - - // Necessary to mount ComponentRegistry members in ScalatraBootstrap - context.setAttribute("ComponentRegistry", componentRegistry) - this.setHandler(context) - } - - private def logCopyrightHeader(): Unit = { - logger.info(Source.fromInputStream(getClass.getResourceAsStream("/log-license.txt")).mkString) - } - - private def warmup(): Unit = { - if (!props.disableWarmup) { - val warmupStart = System.currentTimeMillis() - logger.info("Starting warmup procedure...") - warmupFunction((path, params) => Warmup.warmupRequest(props.ApplicationPort, path, params)) - val warmupTime = System.currentTimeMillis() - warmupStart - logger.info(s"Warmup procedure finished in ${warmupTime}ms.") - componentRegistry.healthController.setWarmedUp() - } - } - - logCopyrightHeader() - - afterHeaderBeforeStart - - this.setRequestLog(new NdlaRequestLogger()) - setupServletContext() - this.start() - - this.warmup() - - private val startedTime: Long = System.currentTimeMillis() - startMillis - logger.info(s"Started ${props.ApplicationName} server at port ${props.ApplicationPort} in ${startedTime}ms.") -} diff --git a/network/src/main/scala/no/ndla/network/scalatra/NdlaSwaggerSupport.scala b/network/src/main/scala/no/ndla/network/scalatra/NdlaSwaggerSupport.scala deleted file mode 100644 index 9e7556496e..0000000000 --- a/network/src/main/scala/no/ndla/network/scalatra/NdlaSwaggerSupport.scala +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Part of NDLA network. - * Copyright (C) 2022 NDLA - * - * See LICENSE - * - */ - -package no.ndla.network.scalatra - -import com.typesafe.scalalogging.StrictLogging -import no.ndla.common.configuration.HasBaseProps -import org.scalatra.swagger.DataType.ValueDataType -import org.scalatra.swagger.SwaggerSupportSyntax.{ParameterBuilder, SwaggerParameterBuilder} -import org.scalatra.swagger.{ParamType, Parameter, SwaggerSupport} -import org.scalatra.util.NotNothing - -trait NdlaSwaggerSupport { - this: HasBaseProps with NdlaControllerBase => - - trait NdlaSwaggerSupport extends NdlaControllerBase with StrictLogging with SwaggerSupport { - - case class Param[T](paramName: String, description: String) - - protected def asQueryParam[T: Manifest: NotNothing](param: Param[T]): ParameterBuilder[T] = - queryParam[T](param.paramName).description(param.description) - - protected def asHeaderParam[T: Manifest: NotNothing](param: Param[T]): ParameterBuilder[T] = - headerParam[T](param.paramName).description(param.description) - - protected def asPathParam[T: Manifest: NotNothing](param: Param[T]): ParameterBuilder[T] = - pathParam[T](param.paramName).description(param.description) - - protected def asObjectFormParam[T: Manifest: NotNothing](param: Param[T]): SwaggerParameterBuilder = { - val className = manifest[T].runtimeClass.getSimpleName - val modelOpt = models.get(className) - - modelOpt match { - case Some(value) => - formParam(param.paramName, value).description(param.description) - case None => - logger.error(s"${param.paramName} could not be resolved as object formParam, doing regular formParam.") - formParam[T](param.paramName).description(param.description) - } - } - - protected def asFileParam(param: Param[_]): Parameter = - Parameter( - name = param.paramName, - `type` = ValueDataType("file"), - description = Some(param.description), - paramType = ParamType.Form - ) - - } -} diff --git a/network/src/main/scala/no/ndla/network/tapir/auth/TokenUser.scala b/network/src/main/scala/no/ndla/network/tapir/auth/TokenUser.scala index c4593d5872..061c945ba3 100644 --- a/network/src/main/scala/no/ndla/network/tapir/auth/TokenUser.scala +++ b/network/src/main/scala/no/ndla/network/tapir/auth/TokenUser.scala @@ -9,14 +9,13 @@ package no.ndla.network.tapir.auth import cats.implicits._ import no.ndla.network.jwt.JWTExtractor -import no.ndla.network.model.{JWTClaims, NdlaHttpRequest} +import no.ndla.network.model.JWTClaims import sttp.model.HeaderNames import sttp.model.headers.{AuthenticationScheme, WWWAuthenticateChallenge} import sttp.tapir.CodecFormat.TextPlain import sttp.tapir.EndpointInput.{AuthInfo, AuthType} import sttp.tapir._ -import javax.servlet.http.HttpServletRequest import scala.collection.immutable.ListMap import scala.util.{Failure, Success, Try} @@ -79,13 +78,6 @@ object TokenUser { fromExtractor(jWTExtractor, token) } - /* Only for scalatra, function can be removed when we move to tapir everywhere :^) */ - def fromScalatraRequest(request: HttpServletRequest): Try[TokenUser] = { - val token = NdlaHttpRequest(request).getToken.getOrElse("") - val extractor = JWTExtractor(token) - fromExtractor(extractor, token) - } - implicit class MaybeTokenUser(self: Option[TokenUser]) { def hasPermission(permission: Permission): Boolean = self.exists(user => user.hasPermission(permission)) } diff --git a/oembed-proxy/src/main/scala/no/ndla/oembedproxy/ComponentRegistry.scala b/oembed-proxy/src/main/scala/no/ndla/oembedproxy/ComponentRegistry.scala index 475e699ded..7ffc414997 100644 --- a/oembed-proxy/src/main/scala/no/ndla/oembedproxy/ComponentRegistry.scala +++ b/oembed-proxy/src/main/scala/no/ndla/oembedproxy/ComponentRegistry.scala @@ -11,7 +11,6 @@ package no.ndla.oembedproxy import no.ndla.common.Clock import no.ndla.common.configuration.BaseComponentRegistry import no.ndla.network.NdlaClient -import no.ndla.network.scalatra.{NdlaControllerBase, NdlaSwaggerSupport} import no.ndla.network.tapir.{NdlaMiddleware, Routes, Service, TapirHealthController} import no.ndla.oembedproxy.caching.MemoizeHelpers import no.ndla.oembedproxy.controller.{OEmbedProxyController, SwaggerDocControllerConfig} @@ -21,8 +20,6 @@ import no.ndla.oembedproxy.service.{OEmbedServiceComponent, ProviderService} class ComponentRegistry(properties: OEmbedProxyProperties) extends BaseComponentRegistry[OEmbedProxyProperties] with OEmbedProxyController - with NdlaControllerBase - with NdlaSwaggerSupport with OEmbedServiceComponent with NdlaClient with ProviderService diff --git a/oembed-proxy/src/test/scala/no/ndla/oembedproxy/TestEnvironment.scala b/oembed-proxy/src/test/scala/no/ndla/oembedproxy/TestEnvironment.scala index 302a324f63..ad42dbe3c2 100644 --- a/oembed-proxy/src/test/scala/no/ndla/oembedproxy/TestEnvironment.scala +++ b/oembed-proxy/src/test/scala/no/ndla/oembedproxy/TestEnvironment.scala @@ -10,7 +10,6 @@ package no.ndla.oembedproxy import no.ndla.common.Clock import no.ndla.network.NdlaClient -import no.ndla.network.scalatra.{NdlaControllerBase, NdlaSwaggerSupport} import no.ndla.network.tapir.{NdlaMiddleware, Routes, Service, TapirHealthController} import no.ndla.oembedproxy.caching.MemoizeHelpers import no.ndla.oembedproxy.controller.OEmbedProxyController @@ -26,8 +25,6 @@ trait TestEnvironment with MockitoSugar with TapirHealthController with Props - with NdlaControllerBase - with NdlaSwaggerSupport with MemoizeHelpers with ErrorHelpers with Clock diff --git a/project/Dependencies.scala b/project/Dependencies.scala index d3a2eadfe5..c00faefc33 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -9,7 +9,6 @@ object Dependencies { object versions { val ScalaV = "2.13.12" - val ScalatraV = "2.8.4" val HikariConnectionPoolV = "5.0.1" val ScalaLoggingV = "3.9.5" val ScalaTestV = "3.2.15" @@ -101,13 +100,6 @@ object Dependencies { lazy val catsEffect: ModuleID = "org.typelevel" %% "cats-effect" % CatsEffectV lazy val tapirHttp4sCirce: Seq[sbt.ModuleID] = circe ++ http4s ++ tapir :+ catsEffect - lazy val scalatra: Seq[ModuleID] = Seq( - "org.scalatra" %% "scalatra" % ScalatraV, - "org.scalatra" %% "scalatra-json" % ScalatraV, - "org.scalatra" %% "scalatra-swagger" % ScalatraV, - "org.scalatra" %% "scalatra-scalatest" % ScalatraV % "test" - ) - lazy val elastic4s: Seq[ModuleID] = Seq( "com.sksamuel.elastic4s" %% "elastic4s-client-esjava" % Elastic4sV, "com.sksamuel.elastic4s" %% "elastic4s-testkit" % Elastic4sV % "test" diff --git a/project/Module.scala b/project/Module.scala index 3cf762f8c8..15b6647018 100644 --- a/project/Module.scala +++ b/project/Module.scala @@ -136,7 +136,7 @@ trait Module { val artifactTargetPath = s"/app/${artifact.name}" val entry = - s"java -Dorg.scalatra.environment=production $$JAVA_OPTS ${reflectiveAccessOptions.mkString(" ")} -jar $artifactTargetPath" + s"java $$JAVA_OPTS ${reflectiveAccessOptions.mkString(" ")} -jar $artifactTargetPath" new Dockerfile { from("eclipse-temurin:21-jdk") diff --git a/project/commonlib.scala b/project/commonlib.scala index fab30ebe06..470836af70 100644 --- a/project/commonlib.scala +++ b/project/commonlib.scala @@ -28,7 +28,6 @@ object commonlib extends Module { "com.amazonaws" % "aws-java-sdk-s3" % AwsSdkV ), melody, - scalatra, tapirHttp4sCirce ) val commonTestExcludeOptions = Set(ScalacOptions.warnUnusedPatVars) diff --git a/project/learningpathapi.scala b/project/learningpathapi.scala index feeda4b397..12364070f8 100644 --- a/project/learningpathapi.scala +++ b/project/learningpathapi.scala @@ -36,7 +36,7 @@ object learningpathapi extends Module { melody, elastic4s, database, - scalatra, + tapirHttp4sCirce, vulnerabilityOverrides ) @@ -49,7 +49,6 @@ object learningpathapi extends Module { ), exports = Seq( "Author", - "Error", "LearningPathStatus", "LearningPathSummaryV2", "LearningPathTagsSummary", diff --git a/project/networklib.scala b/project/networklib.scala index 6757512928..af9ad2dcfe 100644 --- a/project/networklib.scala +++ b/project/networklib.scala @@ -20,7 +20,6 @@ object networklib extends Module { "redis.clients" % "jedis" % "4.4.0" ), vulnerabilityOverrides, - scalatra, tapirHttp4sCirce ) diff --git a/project/oembedproxy.scala b/project/oembedproxy.scala index 1c0fa73568..8f3e99e643 100644 --- a/project/oembedproxy.scala +++ b/project/oembedproxy.scala @@ -22,7 +22,6 @@ object oembedproxy extends Module { "org.mockito" %% "mockito-scala" % MockitoV % "test", "org.mockito" %% "mockito-scala-scalatest" % MockitoV % "test" ), - scalatra, vulnerabilityOverrides, tapirHttp4sCirce ) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/ArticleResult.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/ArticleResult.scala index 924b955837..2870002387 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/ArticleResult.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/ArticleResult.scala @@ -8,18 +8,13 @@ package no.ndla.searchapi.model.api import no.ndla.searchapi.model.api.article.ArticleIntroduction -import org.scalatra.swagger.annotations.ApiModel -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Search result for article api") +@description("Search result for article api") case class ArticleResult( - @(ApiModelProperty @field)(description = "The unique id of this article") id: Long, - @(ApiModelProperty @field)(description = "The title of the article") title: Title, - @(ApiModelProperty @field)(description = "The introduction of the article") introduction: Option[ - ArticleIntroduction - ], - @(ApiModelProperty @field)(description = "The type of the article") articleType: String, - @(ApiModelProperty @field)(description = "List of supported languages") supportedLanguages: Seq[String] + @description("The unique id of this article") id: Long, + @description("The title of the article") title: Title, + @description("The introduction of the article") introduction: Option[ArticleIntroduction], + @description("The type of the article") articleType: String, + @description("List of supported languages") supportedLanguages: Seq[String] ) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/AudioResult.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/AudioResult.scala index 0030655a5c..1ae864383f 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/AudioResult.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/AudioResult.scala @@ -7,15 +7,12 @@ package no.ndla.searchapi.model.api -import org.scalatra.swagger.annotations.ApiModel -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Search result for audio api") +@description("Search result for audio api") case class AudioResult( - @(ApiModelProperty @field)(description = "The unique id of this audio") id: Long, - @(ApiModelProperty @field)(description = "The title of this audio") title: Title, - @(ApiModelProperty @field)(description = "A direct link to the audio") url: String, - @(ApiModelProperty @field)(description = "List of supported languages") supportedLanguages: Seq[String] + @description("The unique id of this audio") id: Long, + @description("The title of this audio") title: Title, + @description("A direct link to the audio") url: String, + @description("List of supported languages") supportedLanguages: Seq[String] ) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/Error.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/Error.scala index b30ec0a09d..3568c60086 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/Error.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/Error.scala @@ -13,32 +13,30 @@ import no.ndla.common.errors.AccessDeniedException import no.ndla.network.tapir.{AllErrors, TapirErrorHelpers} import no.ndla.search.{IndexNotFoundException, NdlaSearchException} import no.ndla.searchapi.Props +import sttp.tapir.Schema.annotations.description import java.time.LocalDateTime -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} -import scala.annotation.meta.field - -@ApiModel(description = "Information about an error") +@description("Information about an error") case class Error( - @(ApiModelProperty @field)(description = "Code stating the type of error") code: String, - @(ApiModelProperty @field)(description = "Description of the error") description: String, - @(ApiModelProperty @field)(description = "An optional id referring to the cover") id: Option[Long] = None, - @(ApiModelProperty @field)(description = "When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() + @description("Code stating the type of error") code: String, + @description("Description of the error") description: String, + @description("An optional id referring to the cover") id: Option[Long] = None, + @description("When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() ) -@ApiModel(description = "Information about validation errors") +@description("Information about validation errors") case class ValidationError( - @(ApiModelProperty @field)(description = "Code stating the type of error") code: String, - @(ApiModelProperty @field)(description = "Description of the error") description: String, - @(ApiModelProperty @field)(description = "List of validation messages") messages: Seq[ValidationMessage], - @(ApiModelProperty @field)(description = "When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() + @description("Code stating the type of error") code: String, + @description("Description of the error") description: String, + @description("List of validation messages") messages: Seq[ValidationMessage], + @description("When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() ) -@ApiModel(description = "A message describing a validation error on a specific field") +@description("A message describing a validation error on a specific field") case class ValidationMessage( - @(ApiModelProperty @field)(description = "The field the error occured in") field: String, - @(ApiModelProperty @field)(description = "The validation message") message: String + @description("The field the error occured in") field: String, + @description("The validation message") message: String ) trait ErrorHelpers extends TapirErrorHelpers { diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/ImageAltText.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/ImageAltText.scala index 4c7123d384..576ce71a8d 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/ImageAltText.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/ImageAltText.scala @@ -7,14 +7,10 @@ package no.ndla.searchapi.model.api import no.ndla.language.model.WithLanguage -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Title of resource") +@description("Title of resource") case class ImageAltText( - @(ApiModelProperty @field)(description = "The freetext alttext of the image") altText: String, - @(ApiModelProperty @field)( - description = "ISO 639-1 code that represents the language used in alttext" - ) language: String + @description("The freetext alttext of the image") altText: String, + @description("ISO 639-1 code that represents the language used in alttext") language: String ) extends WithLanguage diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/ImageResult.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/ImageResult.scala index bd9a42d805..a7ba3fa3d3 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/ImageResult.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/ImageResult.scala @@ -7,17 +7,14 @@ package no.ndla.searchapi.model.api -import org.scalatra.swagger.annotations.ApiModel -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Search result for image api") +@description("Search result for image api") case class ImageResult( - @(ApiModelProperty @field)(description = "The unique id of this image") id: Long, - @(ApiModelProperty @field)(description = "The title of this image") title: Title, - @(ApiModelProperty @field)(description = "The alt text of this image") altText: ImageAltText, - @(ApiModelProperty @field)(description = "A direct link to the image") previewUrl: String, - @(ApiModelProperty @field)(description = "A link to get meta data related to the image") metaUrl: String, - @(ApiModelProperty @field)(description = "List of supported languages") supportedLanguages: Seq[String] + @description("The unique id of this image") id: Long, + @description("The title of this image") title: Title, + @description("The alt text of this image") altText: ImageAltText, + @description("A direct link to the image") previewUrl: String, + @description("A link to get meta data related to the image") metaUrl: String, + @description("List of supported languages") supportedLanguages: Seq[String] ) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/LearningPathIntroduction.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/LearningPathIntroduction.scala index 4b36e79dd9..d875c7cd40 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/LearningPathIntroduction.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/LearningPathIntroduction.scala @@ -7,14 +7,10 @@ package no.ndla.searchapi.model.api import no.ndla.language.model.WithLanguage -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Introduction of learningPath") +@description("Introduction of learningPath") case class LearningPathIntroduction( - @(ApiModelProperty @field)(description = "The freetext introduction of the learningpath") introduction: String, - @(ApiModelProperty @field)( - description = "ISO 639-1 code that represents the language used in introduction" - ) language: String + @description("The freetext introduction of the learningpath") introduction: String, + @description("ISO 639-1 code that represents the language used in introduction") language: String ) extends WithLanguage diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/LearningpathResult.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/LearningpathResult.scala index 8f41e10f39..49f72a8a4a 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/LearningpathResult.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/LearningpathResult.scala @@ -7,17 +7,12 @@ package no.ndla.searchapi.model.api -import org.scalatra.swagger.annotations.ApiModel -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Search result for learningpath api") +@description("Search result for learningpath api") case class LearningpathResult( - @(ApiModelProperty @field)(description = "The unique id of this learningpath") id: Long, - @(ApiModelProperty @field)(description = "The title of the learningpath") title: Title, - @(ApiModelProperty @field)( - description = "The introduction of the learningpath" - ) introduction: LearningPathIntroduction, - @(ApiModelProperty @field)(description = "List of supported languages") supportedLanguages: Seq[String] + @description("The unique id of this learningpath") id: Long, + @description("The title of the learningpath") title: Title, + @description("The introduction of the learningpath") introduction: LearningPathIntroduction, + @description("List of supported languages") supportedLanguages: Seq[String] ) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/SearchResult.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/SearchResult.scala index b2e205e551..dd4d01b2f6 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/SearchResult.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/SearchResult.scala @@ -7,15 +7,13 @@ package no.ndla.searchapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Information about search-results") +@description("Information about search-results") case class SearchResult[T]( - @(ApiModelProperty @field)(description = "The total number of articles matching this query") totalCount: Long, - @(ApiModelProperty @field)(description = "For which page results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The chosen search language") language: String, - @(ApiModelProperty @field)(description = "The search results") results: Seq[T] + @description("The total number of articles matching this query") totalCount: Long, + @description("For which page results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The chosen search language") language: String, + @description("The search results") results: Seq[T] ) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/SearchResults.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/SearchResults.scala index 80520bcf69..416e117480 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/SearchResults.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/SearchResults.scala @@ -7,54 +7,52 @@ package no.ndla.searchapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description sealed trait SearchResults -@ApiModel(description = "Search result for article api") +@description("Search result for article api") case class ArticleResults( - @(ApiModelProperty @field)(description = "The type of search results (articles)") `type`: String, - @(ApiModelProperty @field)(description = "The language of the search results") language: String, - @(ApiModelProperty @field)(description = "The total number of articles matching this query") totalCount: Long, - @(ApiModelProperty @field)(description = "The page from which results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The actual search results") results: Seq[ArticleResult] + @description("The type of search results (articles)") `type`: String, + @description("The language of the search results") language: String, + @description("The total number of articles matching this query") totalCount: Long, + @description("The page from which results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The actual search results") results: Seq[ArticleResult] ) extends SearchResults -@ApiModel(description = "Search result for learningpath api") +@description("Search result for learningpath api") case class LearningpathResults( - @(ApiModelProperty @field)(description = "The type of search results (learningpaths)") `type`: String, - @(ApiModelProperty @field)(description = "The language of the search results") language: String, - @(ApiModelProperty @field)(description = "The total number of learningpaths matching this query") totalCount: Long, - @(ApiModelProperty @field)(description = "The page from which results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The actual search results") results: Seq[LearningpathResult] + @description("The type of search results (learningpaths)") `type`: String, + @description("The language of the search results") language: String, + @description("The total number of learningpaths matching this query") totalCount: Long, + @description("The page from which results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The actual search results") results: Seq[LearningpathResult] ) extends SearchResults -@ApiModel(description = "Search result for image api") +@description("Search result for image api") case class ImageResults( - @(ApiModelProperty @field)(description = "The type of search results (images)") `type`: String, - @(ApiModelProperty @field)(description = "The language of the search results") language: String, - @(ApiModelProperty @field)(description = "The total number of images matching this query") totalCount: Long, - @(ApiModelProperty @field)(description = "The page from which results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The actual search results") results: Seq[ImageResult] + @description("The type of search results (images)") `type`: String, + @description("The language of the search results") language: String, + @description("The total number of images matching this query") totalCount: Long, + @description("The page from which results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The actual search results") results: Seq[ImageResult] ) extends SearchResults -@ApiModel(description = "Search result for audio api") +@description("Search result for audio api") case class AudioResults( - @(ApiModelProperty @field)(description = "The type of search results (audios)") `type`: String, - @(ApiModelProperty @field)(description = "The language of the search results") language: String, - @(ApiModelProperty @field)(description = "The total number of audios matching this query") totalCount: Long, - @(ApiModelProperty @field)(description = "The page from which results are shown from") page: Int, - @(ApiModelProperty @field)(description = "The number of results per page") pageSize: Int, - @(ApiModelProperty @field)(description = "The actual search results") results: Seq[AudioResult] + @description("The type of search results (audios)") `type`: String, + @description("The language of the search results") language: String, + @description("The total number of audios matching this query") totalCount: Long, + @description("The page from which results are shown from") page: Int, + @description("The number of results per page") pageSize: Int, + @description("The actual search results") results: Seq[AudioResult] ) extends SearchResults -@ApiModel(description = "Description of an error when communicating with an api") +@description("Description of an error when communicating with an api") case class SearchError( - @(ApiModelProperty @field)(description = "The api where the error occurred") `type`: String, - @(ApiModelProperty @field)(description = "An error message describing the error") errorMsg: String + @description("The api where the error occurred") `type`: String, + @description("An error message describing the error") errorMsg: String ) extends SearchResults diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/Subject.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/Subject.scala index c2fad93982..11cf353c96 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/Subject.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/Subject.scala @@ -7,13 +7,11 @@ package no.ndla.searchapi.model.api -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Short summary of information about the subject") +@description("Short summary of information about the subject") case class Subject( - @(ApiModelProperty @field)(description = "The name of the subject") name: String, - @(ApiModelProperty @field)(description = "The path to the article") path: String, - @(ApiModelProperty @field)(description = "List of breadcrumbs to article") breadcrumbs: Seq[String] + @description("The name of the subject") name: String, + @description("The path to the article") path: String, + @description("List of breadcrumbs to article") breadcrumbs: Seq[String] ) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleIntroduction.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleIntroduction.scala index 74e1964d4c..be06ac57b4 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleIntroduction.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleIntroduction.scala @@ -8,14 +8,12 @@ package no.ndla.searchapi.model.api.article import no.ndla.language.model.WithLanguage -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Description of the article introduction") +@description("Description of the article introduction") case class ArticleIntroduction( - @(ApiModelProperty @field)(description = "The introduction content") introduction: String, - @(ApiModelProperty @field)( - description = "The ISO 639-1 language code describing which article translation this introduction belongs to" + @description("The introduction content") introduction: String, + @description( + "The ISO 639-1 language code describing which article translation this introduction belongs to" ) language: String ) extends WithLanguage diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleMetaImage.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleMetaImage.scala index 28064a38d9..1615bc8b75 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleMetaImage.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleMetaImage.scala @@ -8,16 +8,14 @@ package no.ndla.searchapi.model.api.article import no.ndla.language.model.LanguageField -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Meta description of the article") +@description("Meta description of the article") case class ArticleMetaImage( - @(ApiModelProperty @field)(description = "The meta image url") url: String, - @(ApiModelProperty @field)(description = "The alt text for the meta image") alt: String, - @(ApiModelProperty @field)( - description = "The ISO 639-1 language code describing which article translation this meta description belongs to" + @description("The meta image url") url: String, + @description("The alt text for the meta image") alt: String, + @description( + "The ISO 639-1 language code describing which article translation this meta description belongs to" ) language: String ) extends LanguageField[String] { override def value: String = url diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleSummary.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleSummary.scala index ac3880d0a4..76a38b83a6 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleSummary.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/article/ArticleSummary.scala @@ -8,30 +8,18 @@ package no.ndla.searchapi.model.api.article import no.ndla.searchapi.model.api.{MetaDescription, Title} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Short summary of information about the article") +@description("Short summary of information about the article") case class ArticleSummary( - @(ApiModelProperty @field)(description = "The unique id of the article") id: Long, - @(ApiModelProperty @field)(description = "The title of the article") title: Title, - @(ApiModelProperty @field)(description = "A visual element article") visualElement: Option[VisualElement], - @(ApiModelProperty @field)(description = "An introduction for the article") introduction: Option[ - ArticleIntroduction - ], - @(ApiModelProperty @field)(description = "A metaDescription for the article") metaDescription: Option[ - MetaDescription - ], - @(ApiModelProperty @field)(description = "A meta image for the article") metaImage: Option[ArticleMetaImage], - @(ApiModelProperty @field)( - description = "The full url to where the complete information about the article can be found" - ) url: String, - @(ApiModelProperty @field)(description = "Describes the license of the article") license: String, - @(ApiModelProperty @field)( - description = "The type of article this is. Possible values are topic-article,standard" - ) articleType: String, - @(ApiModelProperty @field)(description = "A list of available languages for this article") supportedLanguages: Seq[ - String - ] + @description("The unique id of the article") id: Long, + @description("The title of the article") title: Title, + @description("A visual element article") visualElement: Option[VisualElement], + @description("An introduction for the article") introduction: Option[ArticleIntroduction], + @description("A metaDescription for the article") metaDescription: Option[MetaDescription], + @description("A meta image for the article") metaImage: Option[ArticleMetaImage], + @description("The full url to where the complete information about the article can be found") url: String, + @description("Describes the license of the article") license: String, + @description("The type of article this is. Possible values are topic-article,standard") articleType: String, + @description("A list of available languages for this article") supportedLanguages: Seq[String] ) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/article/VisualElement.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/article/VisualElement.scala index d63f81e104..6a3b99dad9 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/article/VisualElement.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/article/VisualElement.scala @@ -8,17 +8,14 @@ package no.ndla.searchapi.model.api.article import no.ndla.language.model.WithLanguage -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Description of a visual element") +@description("Description of a visual element") case class VisualElement( - @(ApiModelProperty @field)( - description = "Html containing the visual element. May contain any legal html element, including the embed-tag" + @description( + "Html containing the visual element. May contain any legal html element, including the embed-tag" ) visualElement: String, - @(ApiModelProperty @field)( - description = "The ISO 639-1 language code describing which article translation this visual element belongs to" + @description( + "The ISO 639-1 language code describing which article translation this visual element belongs to" ) language: String ) extends WithLanguage diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/draft/DraftSummary.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/draft/DraftSummary.scala index ef65efc448..4f6612aded 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/draft/DraftSummary.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/draft/DraftSummary.scala @@ -10,22 +10,19 @@ package no.ndla.searchapi.model.api.draft import no.ndla.common.model.api.draft.Comment import no.ndla.searchapi.model.api.Title import no.ndla.searchapi.model.api.article.{ArticleIntroduction, VisualElement} -import org.scalatra.swagger.annotations.ApiModel -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description // format: off -@ApiModel(description = "Short summary of information about the article") +@description("Short summary of information about the article") case class DraftSummary( - @(ApiModelProperty @field)(description = "The unique id of the article") id: Long, - @(ApiModelProperty @field)(description = "The title of the article") title: Title, - @(ApiModelProperty @field)(description = "A visual element article") visualElement: Option[VisualElement], - @(ApiModelProperty @field)(description = "An introduction for the article") introduction: Option[ArticleIntroduction], - @(ApiModelProperty @field)(description = "The full url to where the complete information about the article can be found") url: String, - @(ApiModelProperty @field)(description = "Describes the license of the article") license: String, - @(ApiModelProperty @field)(description = "The type of article this is. Possible values are topic-article,standard") articleType: String, - @(ApiModelProperty @field)(description = "A list of available languages for this audio") supportedLanguages: Seq[String], - @(ApiModelProperty @field)(description = "The notes for this draft article") notes: Seq[String], - @(ApiModelProperty @field)(description = "Information about comments attached to the article") comments: Seq[Comment] + @description("The unique id of the article") id: Long, + @description("The title of the article") title: Title, + @description("A visual element article") visualElement: Option[VisualElement], + @description("An introduction for the article") introduction: Option[ArticleIntroduction], + @description("The full url to where the complete information about the article can be found") url: String, + @description("Describes the license of the article") license: String, + @description("The type of article this is. Possible values are topic-article,standard") articleType: String, + @description("A list of available languages for this audio") supportedLanguages: Seq[String], + @description("The notes for this draft article") notes: Seq[String], + @description("Information about comments attached to the article") comments: Seq[Comment] ) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Copyright.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Copyright.scala index 0702b374d0..1084ad205a 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Copyright.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Copyright.scala @@ -8,13 +8,10 @@ package no.ndla.searchapi.model.api.learningpath import no.ndla.common.model.api.{Author, License} -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "Description of copyright information") +@description("Description of copyright information") case class Copyright( - @(ApiModelProperty @field)(description = "Describes the license of the learningpath") license: License, - @(ApiModelProperty @field)(description = "List of authors") contributors: Seq[Author] + @description("Describes the license of the learningpath") license: License, + @description("List of authors") contributors: Seq[Author] ) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Description.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Description.scala index 1bf813ed93..7cff9758ef 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Description.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Description.scala @@ -8,17 +8,12 @@ package no.ndla.searchapi.model.api.learningpath import no.ndla.language.model.LanguageField -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "The description of the learningpath") +@description("The description of the learningpath") case class Description( - @(ApiModelProperty @field)(description = "The description to the learningpath.") description: String, - @(ApiModelProperty @field)( - description = "ISO 639-1 code that represents the language used in introduction" - ) language: String + @description("The description to the learningpath.") description: String, + @description("ISO 639-1 code that represents the language used in introduction") language: String ) extends LanguageField[String] { override def value: String = description override def isEmpty: Boolean = description.isEmpty diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Introduction.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Introduction.scala index ba5c2c74c0..de4bf5c724 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Introduction.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/Introduction.scala @@ -8,17 +8,10 @@ package no.ndla.searchapi.model.api.learningpath import no.ndla.language.model.WithLanguage -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field - -@ApiModel(description = "The introduction of the learningpath") +@description("The introduction of the learningpath") case class Introduction( - @(ApiModelProperty @field)( - description = "The introduction to the learningpath. Basic HTML allowed" - ) introduction: String, - @(ApiModelProperty @field)( - description = "ISO 639-1 code that represents the language used in introduction" - ) language: String + @description("The introduction to the learningpath. Basic HTML allowed") introduction: String, + @description("ISO 639-1 code that represents the language used in introduction") language: String ) extends WithLanguage diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/LearningPathSummary.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/LearningPathSummary.scala index 6f44e3a6d3..f939582eed 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/LearningPathSummary.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/LearningPathSummary.scala @@ -8,34 +8,25 @@ package no.ndla.searchapi.model.api.learningpath import no.ndla.searchapi.model.api.Title -import org.scalatra.swagger.annotations._ -import org.scalatra.swagger.runtime.annotations.ApiModelProperty +import sttp.tapir.Schema.annotations.description -import scala.annotation.meta.field import java.time.LocalDateTime -@ApiModel(description = "Summary of meta information for a learningpath") +@description("Summary of meta information for a learningpath") case class LearningPathSummary( - @(ApiModelProperty @field)(description = "The unique id of the learningpath") id: Long, - @(ApiModelProperty @field)(description = "The titles of the learningpath") title: Title, - @(ApiModelProperty @field)(description = "The descriptions of the learningpath") description: Description, - @(ApiModelProperty @field)(description = "The introductions of the learningpath") introduction: Introduction, - @(ApiModelProperty @field)( - description = "The full url to where the complete metainformation about the learningpath can be found" + @description("The unique id of the learningpath") id: Long, + @description("The titles of the learningpath") title: Title, + @description("The descriptions of the learningpath") description: Description, + @description("The introductions of the learningpath") introduction: Introduction, + @description( + "The full url to where the complete metainformation about the learningpath can be found" ) metaUrl: String, - @(ApiModelProperty @field)(description = "Url to where a cover photo can be found") coverPhotoUrl: Option[String], - @(ApiModelProperty @field)(description = "The duration of the learningpath in minutes") duration: Option[Int], - @(ApiModelProperty @field)( - description = "The publishing status of the learningpath.", - allowableValues = "PUBLISHED,PRIVATE,NOT_LISTED" - ) status: String, - @(ApiModelProperty @field)( - description = "The date when this learningpath was last updated." - ) lastUpdated: LocalDateTime, - @(ApiModelProperty @field)(description = "Searchable tags for the learningpath") tags: LearningPathTags, - @(ApiModelProperty @field)(description = "The contributors of this learningpath") copyright: Copyright, - @(ApiModelProperty @field)(description = "A list of available languages for this audio") supportedLanguages: Seq[ - String - ], - @(ApiModelProperty @field)(description = "The id this learningpath is based on, if any") isBasedOn: Option[Long] + @description("Url to where a cover photo can be found") coverPhotoUrl: Option[String], + @description("The duration of the learningpath in minutes") duration: Option[Int], + @description("The publishing status of the learningpath.") status: String, + @description("The date when this learningpath was last updated.") lastUpdated: LocalDateTime, + @description("Searchable tags for the learningpath") tags: LearningPathTags, + @description("The contributors of this learningpath") copyright: Copyright, + @description("A list of available languages for this audio") supportedLanguages: Seq[String], + @description("The id this learningpath is based on, if any") isBasedOn: Option[Long] ) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/LearningPathTags.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/LearningPathTags.scala index 1ff3858fa4..f2236445d8 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/LearningPathTags.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/learningpath/LearningPathTags.scala @@ -8,11 +8,9 @@ package no.ndla.searchapi.model.api.learningpath import no.ndla.language.model.WithLanguage -import org.scalatra.swagger.runtime.annotations.ApiModelProperty - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description case class LearningPathTags( - @(ApiModelProperty @field)(description = "The searchable tags. Must be plain text") tags: Seq[String], - @(ApiModelProperty @field)(description = "ISO 639-1 code that represents the language used in tag") language: String + @description("The searchable tags. Must be plain text") tags: Seq[String], + @description("ISO 639-1 code that represents the language used in tag") language: String ) extends WithLanguage diff --git a/search-api/src/test/scala/no/ndla/searchapi/TestData.scala b/search-api/src/test/scala/no/ndla/searchapi/TestData.scala index 1a5a0d831c..41a8a35900 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/TestData.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/TestData.scala @@ -18,11 +18,11 @@ import no.ndla.common.model.domain.{ Introduction, Priority, Responsible, + Status, Tag, Title, VisualElement, - draft, - Status + draft } import no.ndla.common.model.domain.article.{Article, Copyright} import no.ndla.common.model.domain.draft.{Draft, DraftCopyright, DraftStatus, RevisionMeta, RevisionStatus} @@ -37,10 +37,10 @@ import no.ndla.searchapi.model.grep.{GrepBundle, GrepElement, GrepTitle} import no.ndla.searchapi.model.search._ import no.ndla.searchapi.model.search.settings.{MultiDraftSearchSettings, SearchSettings} import no.ndla.searchapi.model.taxonomy._ -import org.apache.commons.lang3.RandomStringUtils import java.net.URI import java.util.UUID +import scala.util.Random object TestData { @@ -1045,7 +1045,7 @@ object TestData { contextType = contextType, parentIds = context.parentIds :+ parent.id, isPrimary = isPrimary, - contextId = RandomStringUtils.randomAlphabetic(12), + contextId = Random.alphanumeric.take(12).mkString, isVisible = parent.metadata.map(m => m.visible && isVisible).getOrElse(isVisible), isActive = isActive ) diff --git a/search/src/main/scala/no/ndla/search/api/MultiSearchTermsAggregation.scala b/search/src/main/scala/no/ndla/search/api/MultiSearchTermsAggregation.scala index 9f4d8e52b9..84f7469f09 100644 --- a/search/src/main/scala/no/ndla/search/api/MultiSearchTermsAggregation.scala +++ b/search/src/main/scala/no/ndla/search/api/MultiSearchTermsAggregation.scala @@ -9,15 +9,13 @@ package no.ndla.search.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import org.scalatra.swagger.annotations.{ApiModel, ApiModelProperty} - -import scala.annotation.meta.field +import sttp.tapir.Schema.annotations.description // format: off -@ApiModel(description = "Value that appears in the search aggregation") +@description("Value that appears in the search aggregation") case class TermValue( - @(ApiModelProperty @field)(description = "Value that appeared in result") value: String, - @(ApiModelProperty @field)(description = "Number of times the value appeared in result") count: Int + @description("Value that appeared in result") value: String, + @description("Number of times the value appeared in result") count: Int ) object TermValue { @@ -25,13 +23,13 @@ object TermValue { implicit val decoder: Decoder[TermValue] = deriveDecoder } -@ApiModel(description = "Information about search aggregation on `field`") +@description("Information about search aggregation on `field`") case class MultiSearchTermsAggregation( - @(ApiModelProperty @field)(description = "The field the specific aggregation is matching") field: String, - @(ApiModelProperty @field)(description = "Number of documents with values that didn't appear in the aggregation. (Will only happen if there are more than 50 different values)") sumOtherDocCount: Int, - @(ApiModelProperty @field)(description = "The result is approximate, this gives an approximation of potential errors. (Specifics here: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-approximate-counts)") + @description("The field the specific aggregation is matching") field: String, + @description("Number of documents with values that didn't appear in the aggregation. (Will only happen if there are more than 50 different values)") sumOtherDocCount: Int, + @description("The result is approximate, this gives an approximation of potential errors. (Specifics here: https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-approximate-counts)") docCountErrorUpperBound: Int, - @(ApiModelProperty @field)(description = "Values appearing in the field") values: Seq[TermValue] + @description("Values appearing in the field") values: Seq[TermValue] ) // format: on