Skip to content

Commit 795d578

Browse files
committed
typescript: Post process openapi hack to allow nullable types
1 parent b6e530a commit 795d578

File tree

18 files changed

+107
-112
lines changed

18 files changed

+107
-112
lines changed

article-api/src/test/scala/no/ndla/articleapi/db/migration/V59__EnsureWrappingSectionTest.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class V59__EnsureWrappingSectionTest extends UnitSuite with TestEnvironment {
2121
}
2222

2323
test("That the content should not be wrapped if there exists a <section>") {
24-
val migration = new V59__EnsureWrappingSection
24+
val migration = new V59__EnsureWrappingSection
2525
val validArticle = """<section>Dette er en forklaringsartikkel.</section><p>Her er et paragraf.</p>"""
2626

2727
migration.convertContent(validArticle, "nb") should be(validArticle)

common/src/main/scala/no/ndla/common/model/api/FrontPageDTO.scala

-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import io.circe.syntax.EncoderOps
1414
import io.circe.{Decoder, Encoder}
1515
import sttp.tapir.Schema.annotations.description
1616

17-
import scala.annotation.unused
18-
1917
@description("The Menu object")
2018
case class MenuDTO(
2119
@description("Id of the article") articleId: Long,

common/src/main/scala/no/ndla/common/model/api/UpdateOrDelete.scala

+40
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
*/
88
package no.ndla.common.model.api
99

10+
import cats.implicits.catsSyntaxOptionId
1011
import io.circe.{Decoder, Encoder, FailedCursor, Json}
12+
import sttp.tapir.{FieldName, Schema, SchemaType}
13+
1114
import java.util.UUID
1215

1316
/** To handle `null` and `undefined` differently on `PATCH` endpoints
@@ -24,6 +27,43 @@ case object Delete extends UpdateOrDelete[Nothing]
2427
final case class UpdateWith[A](value: A) extends UpdateOrDelete[A]
2528

2629
object UpdateOrDelete {
30+
val schemaName = s"UpdateOrDeleteInnerSchema-${UUID.randomUUID()}"
31+
32+
def replaceSchema(schema: sttp.apispec.Schema): Option[sttp.apispec.Schema] = {
33+
val updateOrDeleteSchema = schema.properties.find { case (k, _) =>
34+
k == UpdateOrDelete.schemaName
35+
}
36+
37+
updateOrDeleteSchema match {
38+
case Some((_, v: sttp.apispec.Schema)) =>
39+
v
40+
.copy(
41+
title = schema.title,
42+
description = schema.description,
43+
deprecated = schema.deprecated
44+
)
45+
.nullable
46+
.some
47+
case _ => None
48+
}
49+
}
50+
51+
implicit def schema[T](implicit subschema: Schema[T]): Schema[UpdateOrDelete[T]] = {
52+
val st: SchemaType.SProduct[UpdateOrDelete[T]] = SchemaType.SProduct(
53+
List(
54+
SchemaType.SProductField[UpdateOrDelete[T], Any](
55+
FieldName(schemaName),
56+
subschema.as,
57+
_ => throw new RuntimeException("This is a bug")
58+
)
59+
)
60+
)
61+
62+
subschema.asOption
63+
.as[UpdateOrDelete[T]]
64+
.copy(schemaType = st)
65+
}
66+
2767
implicit def decodeUpdateOrDelete[A](implicit decodeA: Decoder[A]): Decoder[UpdateOrDelete[A]] =
2868
Decoder.withReattempt {
2969
case c: FailedCursor if !c.incorrectFocus => Right(Missing)

concept-api/src/main/scala/no/ndla/conceptapi/model/api/UpdatedConceptDTO.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ package no.ndla.conceptapi.model.api
1111
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}
1212
import io.circe.{Decoder, Encoder}
1313
import no.ndla.common.model.api.{DraftCopyrightDTO, UpdateOrDelete}
14-
import sttp.tapir.Schema.annotations.description
14+
import sttp.tapir.Schema.annotations.{description, deprecated}
1515

1616
@description("Information about the concept")
1717
case class UpdatedConceptDTO(
@@ -32,6 +32,7 @@ case class UpdatedConceptDTO(
3232
@description("A visual element for the concept. May be anything from an image to a video or H5P")
3333
visualElement: Option[String],
3434
@description("NDLA ID representing the editor responsible for this article")
35+
@deprecated
3536
responsibleId: UpdateOrDelete[String],
3637
@description("Type of concept. 'concept', or 'gloss'")
3738
conceptType: Option[String],

network/src/main/scala/no/ndla/network/tapir/SwaggerControllerConfig.scala

+44-8
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,14 @@
99
package no.ndla.network.tapir
1010

1111
import cats.implicits.*
12+
import io.circe.Json
1213
import no.ndla.common.configuration.HasBaseProps
14+
import no.ndla.common.model.api.UpdateOrDelete
15+
import sttp.apispec
1316
import sttp.apispec.openapi.{Components, Contact, Info, License}
14-
import sttp.apispec.{OAuthFlow, OAuthFlows, SecurityScheme}
17+
import sttp.apispec.{AnySchema, OAuthFlow, OAuthFlows, SchemaLike, SecurityScheme}
1518
import sttp.tapir.*
16-
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter
19+
import sttp.tapir.docs.openapi.{OpenAPIDocsInterpreter, OpenAPIDocsOptions}
1720
import sttp.tapir.server.ServerEndpoint
1821

1922
import scala.collection.immutable.ListMap
@@ -60,16 +63,49 @@ trait SwaggerControllerConfig {
6063
openIdConnectUrl = None
6164
)
6265

63-
private val docs = {
64-
val docs = OpenAPIDocsInterpreter().serverEndpointsToOpenAPI(swaggerEndpoints, info)
66+
/** NOTE: This is a hack to allow us to create nullable types in the specification. If possible this should probably
67+
* be replaced by a tapir alternative when that is possible.
68+
*
69+
* https://github.com/softwaremill/tapir/issues/2953
70+
*/
71+
private val schemaPostProcessingFunctions: List[apispec.Schema => Option[apispec.Schema]] = List(
72+
UpdateOrDelete.replaceSchema
73+
)
74+
75+
private def postProcessSchema(schema: SchemaLike): SchemaLike = {
76+
schema match {
77+
case schema: AnySchema => schema
78+
case schema: apispec.Schema =>
79+
val convertedSchema = schemaPostProcessingFunctions.foldLeft(None: Option[apispec.Schema]) {
80+
case (None, f) => f(schema)
81+
case (Some(convertedSchema), _) => Some(convertedSchema)
82+
}
83+
84+
convertedSchema match {
85+
case Some(value) => value
86+
case None =>
87+
val props = schema.properties.map {
88+
case (k, v: apispec.Schema) => k -> postProcessSchema(v)
89+
case (k, v) => k -> v
90+
}
91+
schema.copy(properties = props)
92+
}
93+
}
94+
}
95+
96+
private val docs: Json = {
97+
val options = OpenAPIDocsOptions.default
98+
val docs = OpenAPIDocsInterpreter(options).serverEndpointsToOpenAPI(swaggerEndpoints, info)
6599
val generatedComponents = docs.components.getOrElse(Components.Empty)
66-
val newComponents = generatedComponents.copy(securitySchemes = ListMap("oauth2" -> Right(securityScheme)))
67-
val docsWithComponents = docs.components(newComponents).asJson
100+
val newSchemas = generatedComponents.schemas.map { case (k, v) => k -> postProcessSchema(v) }
101+
val newComponents = generatedComponents.copy(
102+
securitySchemes = ListMap("oauth2" -> Right(securityScheme)),
103+
schemas = newSchemas
104+
)
105+
val docsWithComponents = docs.components(newComponents).asJson
68106
docsWithComponents.asJson
69107
}
70108

71-
def printSwagger(): Unit = println(docs.noSpaces)
72-
73109
def saveSwagger(): Unit = {
74110
import java.io.*
75111
val swaggerLocation = new File(s"./typescript/types-backend/openapi")

project/commonlib.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ object commonlib extends Module {
1212
enumeratum,
1313
enumeratumCirce,
1414
sttp,
15-
scalikejdbc,
15+
scalikejdbc
1616
),
1717
awsS3,
1818
awsTranscribe,

search-api/src/test/scala/no/ndla/searchapi/service/search/MultiSearchServiceAtomicTest.scala

-1
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,6 @@ class MultiSearchServiceAtomicTest extends IntegrationSuite(EnableElasticsearchC
780780
url = "/f/snabel-fag/asdf2362"
781781
)
782782
),
783-
784783
contexts = List()
785784
)
786785
) ++

typescript/types-backend/concept-api-openapi.ts

+6-24
Original file line numberDiff line numberDiff line change
@@ -454,8 +454,6 @@ export type components = {
454454
* @enum {string}
455455
*/
456456
ContributorType: "artist" | "cowriter" | "compiler" | "composer" | "correction" | "director" | "distributor" | "editorial" | "facilitator" | "idea" | "illustrator" | "linguistic" | "originator" | "photographer" | "processor" | "publisher" | "reader" | "rightsholder" | "scriptwriter" | "supplier" | "translator" | "writer";
457-
/** Delete */
458-
Delete: Record<string, never>;
459457
/**
460458
* DraftConceptSearchParamsDTO
461459
* @description The search parameters
@@ -596,8 +594,6 @@ export type components = {
596594
Map_String: {
597595
[key: string]: string;
598596
};
599-
/** Missing */
600-
Missing: Record<string, never>;
601597
/**
602598
* MultiSearchTermsAggregationDTO
603599
* @description Information about search aggregation on `field`
@@ -720,24 +716,6 @@ export type components = {
720716
*/
721717
count: number;
722718
};
723-
/**
724-
* UpdateOrDelete_NewConceptMetaImageDTO
725-
* @description An image-api ID for the concept meta image
726-
*/
727-
UpdateOrDelete_NewConceptMetaImageDTO: components["schemas"]["Delete"] | components["schemas"]["Missing"] | components["schemas"]["UpdateWith_NewConceptMetaImageDTO"];
728-
/**
729-
* UpdateOrDelete_String
730-
* @description NDLA ID representing the editor responsible for this article
731-
*/
732-
UpdateOrDelete_String: components["schemas"]["Delete"] | components["schemas"]["Missing"] | components["schemas"]["UpdateWith_String"];
733-
/** UpdateWith_NewConceptMetaImageDTO */
734-
UpdateWith_NewConceptMetaImageDTO: {
735-
value: components["schemas"]["NewConceptMetaImageDTO"];
736-
};
737-
/** UpdateWith_String */
738-
UpdateWith_String: {
739-
value: string;
740-
};
741719
/**
742720
* UpdatedConceptDTO
743721
* @description Information about the concept
@@ -749,7 +727,7 @@ export type components = {
749727
title?: string;
750728
/** @description The content of the concept */
751729
content?: string;
752-
metaImage: components["schemas"]["UpdateOrDelete_NewConceptMetaImageDTO"];
730+
metaImage?: components["schemas"]["NewConceptMetaImageDTO"] | null;
753731
/** @description Describes the copyright information for the concept */
754732
copyright?: components["schemas"]["DraftCopyrightDTO"];
755733
/** @description A list of searchable tags */
@@ -758,7 +736,11 @@ export type components = {
758736
status?: string;
759737
/** @description A visual element for the concept. May be anything from an image to a video or H5P */
760738
visualElement?: string;
761-
responsibleId: components["schemas"]["UpdateOrDelete_String"];
739+
/**
740+
* @deprecated
741+
* @description NDLA ID representing the editor responsible for this article
742+
*/
743+
responsibleId?: string | null;
762744
/** @description Type of concept. 'concept', or 'gloss' */
763745
conceptType?: string;
764746
glossData?: components["schemas"]["GlossDataDTO"];

typescript/types-backend/concept-api.ts

-12
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ export type ConceptTitleDTO = schemas["ConceptTitleDTO"];
2929
export type IConceptTitleDTO = schemas["ConceptTitleDTO"];
3030
export type ContributorType = schemas["ContributorType"];
3131
export type IContributorType = schemas["ContributorType"];
32-
export type Delete = schemas["Delete"];
33-
export type IDelete = schemas["Delete"];
3432
export type DraftConceptSearchParamsDTO = schemas["DraftConceptSearchParamsDTO"];
3533
export type IDraftConceptSearchParamsDTO = schemas["DraftConceptSearchParamsDTO"];
3634
export type DraftCopyrightDTO = schemas["DraftCopyrightDTO"];
@@ -49,8 +47,6 @@ export type Map_List_String = schemas["Map_List_String"];
4947
export type IMap_List_String = schemas["Map_List_String"];
5048
export type Map_String = schemas["Map_String"];
5149
export type IMap_String = schemas["Map_String"];
52-
export type Missing = schemas["Missing"];
53-
export type IMissing = schemas["Missing"];
5450
export type MultiSearchTermsAggregationDTO = schemas["MultiSearchTermsAggregationDTO"];
5551
export type IMultiSearchTermsAggregationDTO = schemas["MultiSearchTermsAggregationDTO"];
5652
export type NewConceptDTO = schemas["NewConceptDTO"];
@@ -67,14 +63,6 @@ export type TagsSearchResultDTO = schemas["TagsSearchResultDTO"];
6763
export type ITagsSearchResultDTO = schemas["TagsSearchResultDTO"];
6864
export type TermValueDTO = schemas["TermValueDTO"];
6965
export type ITermValueDTO = schemas["TermValueDTO"];
70-
export type UpdateOrDelete_NewConceptMetaImageDTO = schemas["UpdateOrDelete_NewConceptMetaImageDTO"];
71-
export type IUpdateOrDelete_NewConceptMetaImageDTO = schemas["UpdateOrDelete_NewConceptMetaImageDTO"];
72-
export type UpdateOrDelete_String = schemas["UpdateOrDelete_String"];
73-
export type IUpdateOrDelete_String = schemas["UpdateOrDelete_String"];
74-
export type UpdateWith_NewConceptMetaImageDTO = schemas["UpdateWith_NewConceptMetaImageDTO"];
75-
export type IUpdateWith_NewConceptMetaImageDTO = schemas["UpdateWith_NewConceptMetaImageDTO"];
76-
export type UpdateWith_String = schemas["UpdateWith_String"];
77-
export type IUpdateWith_String = schemas["UpdateWith_String"];
7866
export type UpdatedConceptDTO = schemas["UpdatedConceptDTO"];
7967
export type IUpdatedConceptDTO = schemas["UpdatedConceptDTO"];
8068
export type ValidationErrorBody = schemas["ValidationErrorBody"];

typescript/types-backend/draft-api-openapi.ts

+3-24
Original file line numberDiff line numberDiff line change
@@ -714,8 +714,6 @@ export type components = {
714714
* @enum {string}
715715
*/
716716
ContributorType: "artist" | "cowriter" | "compiler" | "composer" | "correction" | "director" | "distributor" | "editorial" | "facilitator" | "idea" | "illustrator" | "linguistic" | "originator" | "photographer" | "processor" | "publisher" | "reader" | "rightsholder" | "scriptwriter" | "supplier" | "translator" | "writer";
717-
/** Delete */
718-
Delete: Record<string, never>;
719717
/**
720718
* DisclaimerDTO
721719
* @description The disclaimer of the article
@@ -837,8 +835,6 @@ export type components = {
837835
Map_List_String: {
838836
[key: string]: string[];
839837
};
840-
/** Missing */
841-
Missing: Record<string, never>;
842838
/**
843839
* MultiPartialPublishResultDTO
844840
* @description A list of articles that were partial published to article-api
@@ -1059,24 +1055,6 @@ export type components = {
10591055
/** @description The search results */
10601056
results: string[];
10611057
};
1062-
/**
1063-
* UpdateOrDelete_NewArticleMetaImageDTO
1064-
* @description An image-api ID for the article meta image
1065-
*/
1066-
UpdateOrDelete_NewArticleMetaImageDTO: components["schemas"]["Delete"] | components["schemas"]["Missing"] | components["schemas"]["UpdateWith_NewArticleMetaImageDTO"];
1067-
/**
1068-
* UpdateOrDelete_String
1069-
* @description NDLA ID representing the editor responsible for this article
1070-
*/
1071-
UpdateOrDelete_String: components["schemas"]["Delete"] | components["schemas"]["Missing"] | components["schemas"]["UpdateWith_String"];
1072-
/** UpdateWith_NewArticleMetaImageDTO */
1073-
UpdateWith_NewArticleMetaImageDTO: {
1074-
value: components["schemas"]["NewArticleMetaImageDTO"];
1075-
};
1076-
/** UpdateWith_String */
1077-
UpdateWith_String: {
1078-
value: string;
1079-
};
10801058
/**
10811059
* UpdatedArticleDTO
10821060
* @description Information about the article
@@ -1103,7 +1081,7 @@ export type components = {
11031081
introduction?: string;
11041082
/** @description A meta description */
11051083
metaDescription?: string;
1106-
metaImage: components["schemas"]["UpdateOrDelete_NewArticleMetaImageDTO"];
1084+
metaImage?: components["schemas"]["NewArticleMetaImageDTO"] | null;
11071085
/** @description A visual element for the article. May be anything from an image to a video or H5P */
11081086
visualElement?: string;
11091087
copyright?: components["schemas"]["DraftCopyrightDTO"];
@@ -1127,7 +1105,8 @@ export type components = {
11271105
relatedContent?: (components["schemas"]["RelatedContentLinkDTO"] | number)[];
11281106
/** @description A list of all revisions of the article */
11291107
revisionMeta?: components["schemas"]["RevisionMetaDTO"][];
1130-
responsibleId: components["schemas"]["UpdateOrDelete_String"];
1108+
/** @description NDLA ID representing the editor responsible for this article */
1109+
responsibleId?: string | null;
11311110
/** @description The path to the frontpage article */
11321111
slug?: string;
11331112
/** @description Information about a comment attached to an article */

typescript/types-backend/draft-api.ts

-12
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ export type ContentIdDTO = schemas["ContentIdDTO"];
3535
export type IContentIdDTO = schemas["ContentIdDTO"];
3636
export type ContributorType = schemas["ContributorType"];
3737
export type IContributorType = schemas["ContributorType"];
38-
export type Delete = schemas["Delete"];
39-
export type IDelete = schemas["Delete"];
4038
export type DisclaimerDTO = schemas["DisclaimerDTO"];
4139
export type IDisclaimerDTO = schemas["DisclaimerDTO"];
4240
export type DraftCopyrightDTO = schemas["DraftCopyrightDTO"];
@@ -57,8 +55,6 @@ export type LicenseDTO = schemas["LicenseDTO"];
5755
export type ILicenseDTO = schemas["LicenseDTO"];
5856
export type Map_List_String = schemas["Map_List_String"];
5957
export type IMap_List_String = schemas["Map_List_String"];
60-
export type Missing = schemas["Missing"];
61-
export type IMissing = schemas["Missing"];
6258
export type MultiPartialPublishResultDTO = schemas["MultiPartialPublishResultDTO"];
6359
export type IMultiPartialPublishResultDTO = schemas["MultiPartialPublishResultDTO"];
6460
export type NewArticleDTO = schemas["NewArticleDTO"];
@@ -91,14 +87,6 @@ export type StatusDTO = schemas["StatusDTO"];
9187
export type IStatusDTO = schemas["StatusDTO"];
9288
export type TagsSearchResultDTO = schemas["TagsSearchResultDTO"];
9389
export type ITagsSearchResultDTO = schemas["TagsSearchResultDTO"];
94-
export type UpdateOrDelete_NewArticleMetaImageDTO = schemas["UpdateOrDelete_NewArticleMetaImageDTO"];
95-
export type IUpdateOrDelete_NewArticleMetaImageDTO = schemas["UpdateOrDelete_NewArticleMetaImageDTO"];
96-
export type UpdateOrDelete_String = schemas["UpdateOrDelete_String"];
97-
export type IUpdateOrDelete_String = schemas["UpdateOrDelete_String"];
98-
export type UpdateWith_NewArticleMetaImageDTO = schemas["UpdateWith_NewArticleMetaImageDTO"];
99-
export type IUpdateWith_NewArticleMetaImageDTO = schemas["UpdateWith_NewArticleMetaImageDTO"];
100-
export type UpdateWith_String = schemas["UpdateWith_String"];
101-
export type IUpdateWith_String = schemas["UpdateWith_String"];
10290
export type UpdatedArticleDTO = schemas["UpdatedArticleDTO"];
10391
export type IUpdatedArticleDTO = schemas["UpdatedArticleDTO"];
10492
export type UpdatedCommentDTO = schemas["UpdatedCommentDTO"];

0 commit comments

Comments
 (0)