It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

サーバサイドKotlinでのGraphQLのグローバルIDの変換処理

やりたいこと・背景

サーバサイドKotlin + GraphQLでAPIを作成しています。 GraphQLを書いているとアプリケーション内部のドメインに紐づくモデルを、GraphQLスキーマに変換することは日常的にあるかなと思いますが、このときモデルのIDをGraphQLのグローバルIDに変換するのが手間だと思います。 Kotlinはこのあたり柔軟に書けるのでコードとして書いてみました。

GraphQLのグローバルIDについて、詳細を知りたい方は以下を参照。

GraphQL Global Object Identification Specification

グローバルIDのフォーマット

今回のコードでは、GraphQLのグローバルフォーマットは、<Model名>:<Id(int)> としてこれをBase64エンコードしたものをクライアントに返しています。

例:

  • Userモデル id=123の場合

エンコード前文字列: User:123 エンコード後文字列: VXNlcjoxMjM=

使用しているライブラリ

※ 基本的なコードの構成は、ライブラリに依存するものではありませんが、該当のライブラリのクラスやメソッドをつかっているため記載しています。

コード全体

// GraphQLのライブラリとしてgraphql-kotlinを使用しているためグローバルIDは当該のクラスを使用する
import com.expediagroup.graphql.generator.scalars.ID
// ktorにbase64エンコード/デコード処理が公開されていたため使用(何でも良い)
import io.ktor.util.decodeBase64String
import io.ktor.util.encodeBase64
import <独自のパッケージ名>.ModelId

// ドメインに紐づくモデルのID
// 各モデルごとに継承して使う
open class ModelId<T>(val value: T) {
    override fun toString() = value.toString()

    override fun hashCode(): Int = value.hashCode()

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is ModelId<*>) return false
        return other.value == value
    }
}

// Userモデル用に継承したID
class UserId(value: Int) : ModelId<Int>(value)

data class User(
    val id: UserId,
    val mane: String,
)

// モデルID => GraphQLのグローバルID変換
fun ModelId<Int>.toId(): ID {
    try {
        // モデル毎 のクラス名を取得(ex: Userモデルなら UserId)
        val cName = this::class.simpleName!!
    // suffixが "Id"であることを決め打ちしてモデル名を取得
        // ※ このあたりはサンプルなので適当にしています。
        val modelName = cName.substring(0, cName.length - 2)
    // 「モデル名:id値」で文字列にしてエンコードする
        return ID("$modelName:${this.value}".encodeBase64())
    } catch (e: Throwable) {
        throw IllegalStateException("Encode ID Failure.")
    }
}

// GraphQLのグローバルID => モデルID変換
fun <T : ModelId<Int>> ID.toModelId(): T {
    try {
        // base64からモデルをデコード
    val decodedId = value.decodeBase64String()
    // :で分割してモデル名を取得
        val modelAndId = decodedId.split(":")
    // モデル名 + Id でモデル毎のIdクラスの名前を作る(ex: Userモデルなら UserId)
        val cName = "${modelAndId.first()}Id"
    // リフレクションでインスタンスを生成する
        val fullClass = Class.forName("<独自のモデルのパッケージ名>.$cName")
        val constructor = fullClass.getConstructor(Int::class.java)
        return constructor.newInstance(modelAndId.last().toInt()) as T
    } catch (e: Throwable) {
        throw IllegalArgumentException("Parse ModelId Failure.")
    }
}

使い方

internal class ModelIdExtensionsKtTest : StringSpec({
    "toId: Should return Id by Model." {
        UserId(123).toId() shouldBe ID("UserId:123".encodeBase64())
    }

    "toModelId: Should return ModelId from ID." {
        val modelId = ID("UserId:123".encodeBase64()).toModelId<UserId>()
        modelId.value shouldBe 123
    }
})

感想

このあたり、TypeScriptなど、拡張が手軽にできない言語だと、サクッと書けないのでKotlinは便利です