Skip to content

Commit

Permalink
Merge pull request #1252 from UdashFramework/schema-name-adjust
Browse files Browse the repository at this point in the history
Add @schemaName annotation to customize RestSchema
  • Loading branch information
sebaciv authored Jul 29, 2024
2 parents 1c818e4 + c997669 commit bc04cac
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
package io.udash.rest.openapi

import com.avsystem.commons.Opt
import com.avsystem.commons.{Opt, StaticAnnotation}
import com.avsystem.commons.annotation.NotInheritedFromSealedTypes
import com.avsystem.commons.misc.{AnnotationOf, SimpleClassName}
import com.avsystem.commons.serialization.name

/**
* Can be used on case class or sealed hierarchy root to instruct OpenAPI schema derivation mechanism
* to use particular value as [[RestSchema]] name instead of just using class name.
*
* Takes precedence over [[com.avsystem.commons.serialization.name]], but unlike
* [[com.avsystem.commons.serialization.name]] does <b>NOT</b> change type discriminator value
* when the class is a part of a sealed hierarchy. This is useful to resolve schema name conflicts
* with other classes without changing serialized representation.
*/
class schemaName(val name: String) extends StaticAnnotation with NotInheritedFromSealedTypes

/**
* A metadata typeclass that you can use to control names of [[RestSchema]]s macro-materialized
* for ADTs (case classes & sealed hierarchies).
Expand Down Expand Up @@ -52,10 +64,16 @@ object GeneratedSchemaName extends GeneratedSchemaNameLowPrio {
*/
def some[T](name: String): GeneratedSchemaName[T] = GeneratedSchemaName(Opt(name))

implicit def annotSchemaName[T](implicit nameAnnot: AnnotationOf[name, T]): GeneratedSchemaName[T] =
implicit def annotSchemaName[T](implicit schemaNameAnnot: AnnotationOf[schemaName, T]): GeneratedSchemaName[T] =
GeneratedSchemaName.some(schemaNameAnnot.annot.name)
}

trait GeneratedSchemaNameLowPrio extends GeneratedSchemaNameLowestPrio { this: GeneratedSchemaName.type =>
implicit def annotName[T](implicit nameAnnot: AnnotationOf[name, T]): GeneratedSchemaName[T] =
GeneratedSchemaName.some(nameAnnot.annot.name)
}
trait GeneratedSchemaNameLowPrio { this: GeneratedSchemaName.type =>

trait GeneratedSchemaNameLowestPrio { this: GeneratedSchemaName.type =>
implicit def classSchemaName[T: SimpleClassName]: GeneratedSchemaName[T] =
GeneratedSchemaName.some(SimpleClassName.of[T])
}
Original file line number Diff line number Diff line change
Expand Up @@ -227,11 +227,12 @@ object RestStructure extends AdtMetadataCompanion[RestStructure] {
final case class NameAndAdjusters[T](
@reifyName sourceName: String,
@optional @reifyAnnot annotName: Opt[name],
@optional @reifyAnnot annotSchemaName: Opt[schemaName],
@multi @reifyAnnot schemaAdjusters: List[SchemaAdjuster]
) extends TypedMetadata[T] {
def restSchema(wrappedSchema: RestSchema[_]): RestSchema[T] = RestSchema.create(
r => SchemaAdjuster.adjustRef(schemaAdjusters, r.resolve(wrappedSchema)),
annotName.fold(sourceName)(_.name)
annotSchemaName.map(_.name).orElse(annotName.map(_.name)).getOrElse[String](sourceName),
)
}
object NameAndAdjusters extends AdtMetadataCompanion[NameAndAdjusters]
Expand Down
98 changes: 98 additions & 0 deletions rest/src/test/scala/io/udash/rest/openapi/RestSchemaTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ object FullyQualifiedHierarchy extends RestDataCompanionWithDeps[FullyQualifiedN
case object Baz extends FullyQualifiedHierarchy
}

@flatten("case")
sealed trait CustomSchemaNameHierarchy
object CustomSchemaNameHierarchy extends RestDataCompanion[CustomSchemaNameHierarchy] {
// annotation value should be used as schema name, but NOT as type discriminator value
@schemaName("CustomSchemaName123")
case class CustomSchemaName(str: String) extends CustomSchemaNameHierarchy

// annotation value should be used as both schema name and type discriminator value
@name("CustomName123")
case class CustomName(str: String) extends CustomSchemaNameHierarchy

// @schemaName annotation should be used as schema name, @name annotation should be used only as type discriminator value
@schemaName("CustomSchemaNameBoth") @name("CustomNameBoth123")
case class CustomNameBoth(str: String) extends CustomSchemaNameHierarchy
}

class RestSchemaTest extends AnyFunSuite {
private def schemaStr[T](implicit schema: RestSchema[T]): String =
printSchema(new InliningResolver().resolve(schema))
Expand Down Expand Up @@ -332,4 +348,86 @@ class RestSchemaTest extends AnyFunSuite {
| }
|}""".stripMargin)
}

test("Customized schema name") {
assert(allSchemasStr[CustomSchemaNameHierarchy] ==
"""{
| "CustomName123": {
| "type": "object",
| "properties": {
| "case": {
| "type": "string",
| "enum": [
| "CustomName123"
| ]
| },
| "str": {
| "type": "string"
| }
| },
| "required": [
| "case",
| "str"
| ]
| },
| "CustomSchemaName123": {
| "type": "object",
| "properties": {
| "case": {
| "type": "string",
| "enum": [
| "CustomSchemaName"
| ]
| },
| "str": {
| "type": "string"
| }
| },
| "required": [
| "case",
| "str"
| ]
| },
| "CustomSchemaNameBoth": {
| "type": "object",
| "properties": {
| "case": {
| "type": "string",
| "enum": [
| "CustomNameBoth123"
| ]
| },
| "str": {
| "type": "string"
| }
| },
| "required": [
| "case",
| "str"
| ]
| },
| "CustomSchemaNameHierarchy": {
| "type": "object",
| "oneOf": [
| {
| "$ref": "#/testSchemas/CustomSchemaName123"
| },
| {
| "$ref": "#/testSchemas/CustomName123"
| },
| {
| "$ref": "#/testSchemas/CustomSchemaNameBoth"
| }
| ],
| "discriminator": {
| "propertyName": "case",
| "mapping": {
| "CustomSchemaName": "#/testSchemas/CustomSchemaName123",
| "CustomName123": "#/testSchemas/CustomName123",
| "CustomNameBoth123": "#/testSchemas/CustomSchemaNameBoth"
| }
| }
| }
|}""".stripMargin)
}
}

0 comments on commit bc04cac

Please sign in to comment.