Compare commits

..

No commits in common. "master" and "2024-furikaeri" have entirely different histories.

9 changed files with 1 additions and 788 deletions

View File

@ -18,15 +18,3 @@ name = "技術"
parent = "カテゴリ"
pageRef = "/categories/技術"
weight = 10
[[main]]
name = "旅行"
parent = "カテゴリ"
pageRef = "/categories/旅行"
weight = 20
[[main]]
name = "その他"
parent = "カテゴリ"
pageRef = "/categories/その他"
weight = 100

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 KiB

View File

@ -2,7 +2,7 @@
author: usbharu
draft: false
categories:
- その他
- null
date: 2024-02-17T14:52:17+09:00
tags:
- null

View File

@ -1,120 +0,0 @@
---
author: usbharu
draft: false
categories:
- ボツ記事
date: 2024-12-26T15:05:47+09:00
tags:
- Kubernetes
- Docker
keywords:
- Kubernetes
- Docker
title: ボツ記事 k8s内に構築したPostgreSQLをCronJobでバックアップする
relpermalink: posts/2024-12-26/
url: posts/2024-12-26/
decription: k8s内に構築したPostgreSQLをCronJobでバックアップする
---
[おうちk8sでMisskeyとNowPlayingを動かし始めたよ · usbharu - blog](https://blog.usbharu.dev/posts/2024-12-01/)でCloudNative PostgreSQLをやめ、k8s内で普通のPostgreSQLを動かし始めました。同時にバックアップ手段も失ったので、今回は自動バックアップを構築する記事です。
## やりたいこと
k8sのCronJobでPostgreSQLをバックアップのpg_dumpを作成してオブジェクトストレージにアップロードしたい。
## 出来上がったもの
[usbharu/pg_backup_job - pg_backup_job - Gitea for usbharu](https://git.usbharu.dev/usbharu/pg_backup_job)
今回ほとんどChatGPTに作ってもらいました。これぐらいなら余裕ですね!
コメント等もほぼChatGPTが出力したままです。
```Dockerfile
# ベースイメージにPostgreSQL 15の公式クライアントを使用
FROM postgres:16
# 必要なツールをインストール
RUN apt-get update && apt-get install -y \
curl \
awscli \
&& rm -rf /var/lib/apt/lists/*
# 作業ディレクトリ
WORKDIR /backup
# スクリプトをコピー
COPY backup.sh /backup/backup.sh
# 実行権限を付与
RUN chmod +x /backup/backup.sh
# エントリーポイントを設定
ENTRYPOINT ["/backup/backup.sh"]
```
```bash
#!/bin/bash
set -e
# 必須環境変数リスト
REQUIRED_VARS=(
PG_HOST
PG_PORT
PG_USER
PG_PASSWORD
PG_DATABASE
S3_BUCKET
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
)
# 未設定の環境変数をチェック
for VAR in "${REQUIRED_VARS[@]}"; do
if [ -z "${!VAR}" ]; then
echo "エラー: 必須環境変数 '${VAR}' が設定されていません。" >&2
MISSING_ENV=true
fi
done
if [ "$MISSING_ENV" = true ]; then
exit 1
fi
# 環境変数の設定
export AWS_ACCESS_KEY_ID="${AWS_ACCESS_KEY_ID}"
export AWS_SECRET_ACCESS_KEY="${AWS_SECRET_ACCESS_KEY}"
export AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-us-east-1}"
# ダンプファイル名
DUMP_FILE="dump_$(date +%Y%m%d%H%M%S).sql"
# pg_dumpを実行
export PGPASSWORD="${PG_PASSWORD}"
pg_dump -h "${PG_HOST}" -p "${PG_PORT}" -U "${PG_USER}" -d "${PG_DATABASE}" -F c -b -v -f "${DUMP_FILE}"
# S3にアップロード
if [[ -n "${S3_ENDPOINT}" ]]; then
# カスタムエンドポイント指定時
aws --endpoint-url "${S3_ENDPOINT}" s3 cp "${DUMP_FILE}" "s3://${S3_BUCKET}/${DUMP_FILE}"
else
# デフォルトエンドポイント
aws s3 cp "${DUMP_FILE}" "s3://${S3_BUCKET}/${DUMP_FILE}"
fi
# ローカルのダンプファイルを削除
rm -f "${DUMP_FILE}"
echo "バックアップとアップロードが完了しました。"
```
こっちの方もちょっと変わっていて、CronJobとそれに必要なSecretが追加されています。
[usbharu/usbharu-lab at e067db4ea302c3d3ee63c375d5058e1c2c6bedf1](https://github.com/usbharu/usbharu-lab/tree/e067db4ea302c3d3ee63c375d5058e1c2c6bedf1)
---
- ボツ理由ChatGPTで生成したんなら記事各意味なくね?
- 状態:書きかけ

View File

@ -1,68 +0,0 @@
---
author: usbharu
draft: false
categories:
- 技術
date: 2025-01-06T15:19:35+09:00
tags:
- Kubernetes
- misskey
keywords:
- Kubernetes
- misskey
title: Misskey on k8sと本番環境でやらかしちゃった人
relpermalink: posts/2025-01-06/
url: posts/2025-01-06/
decription: Misskey on k8sと本番環境でやらかしちゃった人
---
あけましておめでとうございます。年末にMisskey on k8sを少し改良しました。ついでにやらかしちゃったので懺悔(?)です。
## 現状
usbharu.devでは、k8sを使ってMisskeyを構築しており、DBにCloudnative PostgreSQL、Redisは普通にDeploymentを使用して構築しています。ArgoCDとHelmのRelease機能を活用してyamlをコピペするだけでドメイン等含めて全自動で新しいMisskeyインスタンスが生えてくる状態になっています。
## 改良
[misskeyの設定をvaluesからできるように · usbharu/usbharu-lab@3f24732](https://github.com/usbharu/usbharu-lab/commit/3f247329f968bc692e3d5e3e4f24c09f48c3d6b0)
[メンテナンスページを出せるように · usbharu/usbharu-lab@6f7afdb](https://github.com/usbharu/usbharu-lab/commit/6f7afdb15307f26a66d3838edd6e1a864f20ae7e)
MisskeyのJobQueueに関する設定をhelmのvalues.yamlから変更できるようになりました。それと、Misskeyをメンテナンスしているときにアクセスが来たときに表示する、メンテナンスページをを簡単に表示できるようになりました。
[メンテナンスページのテストの様子](https://mk.yopo.work/notes/a2jrhk78vg)
それと、バックアップのスケジュールがUTCなのをすっかり忘れて昼間の12時にしていたのでタイムゾーンを設定するようにしました。
```yaml
spec:
timeZone: "Asia/Tokyo"
schedule: {{ cron式k8s版 }}
```
結果的にこのタイムゾーン設定忘れのお陰で救われるんですがそれはまた後ほど。
## やらかし
[negi by usbharu · Pull Request #7 · usbharu/usbharu-lab](https://github.com/usbharu/usbharu-lab/pull/7)
訳合って新しいMisskeyサーバーを生やしたときのPRです。このPRの中にやらかしが混じっています。
前述の通り、コピペするだけで新しくMisskeyを生やすことができるわけですが、正確にはコピペした後変更する必要がある場所があります。例えばドメイン(当然)とか、ArgoCDのApplication名とか。
HelmのRelease名を変えてArgoCDのApplicationの`metadata.name`を変更するのを忘れると、新しく作ったApplication(に常になるのか、優先順位がどうなってるのかは知らん)に上書きされます。当然元のApplicationは消えPostgreSQLのクラスタは削除されあらゆるデータが削除されます。
というのをやらかしました……夜10時ぐらいにPRをマージして、気づいたときには時既にお寿司🍣。元からあった自分のお一人様`misskey.usbharu.dev`が消えました。今まで数分のロールバックはありましたが、DB全部ふっ飛ばしたことは初めてです。幸いk8sの移行後ちゃんと自動バックアップを取るようにしていたので、バックアップが失敗していなければ数時間ロールバックしますが復旧できます。ファイルはオブジェクトストレージにアップロードしているので問題なし。通知とアンテナが消えることはいつものことなので気にしたら負けです。
朝3時に自動バックアップを設定していたので大体20時間ぐらい巻き戻るはず…だったんですが上に書いたようにタイムゾーン間違えてたので結果的には10時間の巻き戻りで済みました。その間にフォロー等はなかったので不整合も最低限です。
## 対策
[うっかり削除しちゃっても大丈夫なようにした · usbharu/usbharu-lab@f276d15](https://github.com/usbharu/usbharu-lab/commit/f276d150cf31aacf4b29308f43c20b19889ef134)
RedisとCloudnativePGのClusterのannotationsにArgoCDが自動でリソースを削除しないよう指示しました。うまく動くかは知りません。
```yaml
annotations:
argocd.argoproj.io/sync-options: Delete=false
```

View File

@ -1,64 +0,0 @@
---
author: usbharu
draft: false
categories:
- その他
date: 2025-01-11T01:33:44+09:00
tags:
- misskey
keywords:
- null
title: 2025年1月11日0時頃から同1時頃までの障害について
relpermalink: posts/2025-01-11/
url: posts/2025-01-11/
decription: 2025年1月11日0時頃から同1時頃にかけて発生した連合先インスタンスからの投稿がタイムラインに表示されない障害についての文章です。(真面目な文章ではありません)
---
## 概要
2025年1月11日0時頃から同1時頃にかけて発生した連合先インスタンスからの投稿がタイムラインに表示されない障害についての文章です。(真面目な文章ではありません)
## 障害発生時刻
2025年1月11日0時頃
## 障害復旧時刻
2025年1月11日1時頃
## 障害内容
`https://negi.usbharu.dev`においてInboxが詰まり、連合先インスタンスからの配送の処理が止まりました。投稿がタイムラインに表示されず、リアクションなども受け取れていませんでした。
## 障害範囲
- `https://negi.usbharu.dev`に存在する全てのローカルアカウント
## 発生原因
多分リモートユーザーの中にピン留めートの数がすごい多い人がいて、制限に引っかかってエラーが発生しそのままリトライになった結果Inboxが爆発したと思われる。ちゃんとした調査はしてない。
## 一次対応
- Inboxのジョブキューの設定を大幅に強化し、パワーでぶん殴りました。
- ジョブの強制リトライを手動実行しました。
- リモートユーザーに対してピン留めノート数の制限を大幅に緩めました。
## 今後の対応
- ジョブキューの監視をおこない早期に対処できるようにします。
- GTLの監視をおこないInboxの問題を早期に対処できるようにします。
## 障害経緯
|タイムスタンプ|事象|
|-|-|
|2025年1月11日 0時頃|Inboxが詰まり始める|
|2025年1月11日 0時52分頃|音ゲーを開始|
|2025年1月11日 0時54分|利用者からのDM|
|2025年1月11日 0時55分|DMをチラ見しながらフルコンを逃す|
|2025年1月11日 0時55分|DMを確認 対応を開始|
|2025年1月11日 0時57分|ジョブキューの設定を変更|
|2025年1月11日 1時00分|変更した設定が反映される|
|2025年1月11日 1時02分|障害解消を確認 利用者にDM|
|2025年1月11日 1時30分頃|障害原因を推定 ピン留めノートの数の制限を緩和|

View File

@ -1,322 +0,0 @@
---
author: usbharu
draft: false
categories:
- 技術
date: 2025-01-12T14:08:58+09:00
tags:
- Kotlin
- ActivityPub
- Kotlin
- ActivityStreams
- ActivityVocabulary
keywords:
- Kotlin
- ActivityPub
- Kotlin
- ActivityStreams
- ActivityVocabulary
title: ActivityStreamsをKotlinでいい感じに扱いたい(デシリアライズ編)
relpermalink: posts/2025-01-12/
url: posts/2025-01-12/
decription: ActivityStreamsをKotlinでいい感じに扱いたい(デシリアライズ編)
---
ActivityPubで使われてる[ActivityStreams](https://www.w3.org/TR/activitystreams-core/)をKotlinでいい感じに扱いたくて、試行錯誤すること数ヶ月。ようやくPoC的なコードが動いたので記事になりました。
[usbharu/activity-streams-serialization at 55ea9a0c858ce4f9be27bc85088a471a627240c0](https://github.com/usbharu/activity-streams-serialization/tree/55ea9a0c858ce4f9be27bc85088a471a627240c0)
## ActivityStreamsの簡単な解説
ActivityStreamsは主に2つの仕様を組み合わせて作られています。
### JSON-LD
JSONでXMLみたいなことをするやつで、検索するとカスSEO業者のクソ記事しか出てこないので理解に苦労しました。XMLの真似事してるだけあってRDFだかなんだか色々出てきます。雑に言うとJSONに共通の型をつけて、URIみたいなので管理する仕組みです。
### Activity Vocabulary
ActivityStreamsのJSON-LDのプロパティとかを色々決めてるやつで、ここに書いてあるやつを実装したら(ActivityVocabularyとしては)9割ぐらい完成です。残り1割はなんか書いてないけど必要だったりするやつです。
## ActivityStreamsの実例
JSON-LD Playgroundにある例をそのまま持ってきますが、
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"@type": "Create",
"actor": {
"@type": "Person",
"@id": "acct:sally@example.org",
"name": "Sally"
},
"object": {
"@type": "Note",
"content": "This is a simple note"
},
"published": "2015-01-25T12:34:56Z"
}
```
というJSONがあったとして、これをJSON-LDのExpandedな形に変形すると
```json
[
{
"@type": [
"https://www.w3.org/ns/activitystreams#Create"
],
"https://www.w3.org/ns/activitystreams#actor": [
{
"@id": "acct:sally@example.org",
"@type": [
"https://www.w3.org/ns/activitystreams#Person"
],
"https://www.w3.org/ns/activitystreams#name": [
{
"@value": "Sally"
}
]
}
],
"https://www.w3.org/ns/activitystreams#object": [
{
"@type": [
"https://www.w3.org/ns/activitystreams#Note"
],
"https://www.w3.org/ns/activitystreams#content": [
{
"@value": "This is a simple note"
}
]
}
],
"https://www.w3.org/ns/activitystreams#published": [
{
"@type": "http://www.w3.org/2001/XMLSchema#dateTime",
"@value": "2015-01-25T12:34:56Z"
}
]
}
]
```
こうなります。ActivityPub実装してるときに最初にぶち当たる壁って大体署名で次がJSONの形が自由すぎて意味が分からん!!だと思うんですが(n=1)、JSON-LDでちゃんと拡張してあげると決まった形になります。`@type`が配列って時点でほとんどのJSONシリアライザが使えないということがわかると思います。
## 実装時の苦労ポイント
### やっぱりJSONが自由すぎる問題
JSON-LDになってこれで自由なJSON問題は解決とか思ってたら次の壁にぶち当たります。そう、各プロパティに入ってくる型がめっちゃ自由です。例えば`Object`型の`attributedTo`プロパティ、W3Cの勧告みると`Object`か`Link`だよ!って書いてあるんですが要は`Note`かもしれないし`Person`かもしれないし`Create`かもしれないし`Link`と`Object`両方かもしれないってことです。なんならActivityStreams意外のJSON-LDの型も含まれてるかもしれません。
KotlinでJSONをシリアライズ・デシリアライズするときって大体data class作ってそこにいい感じにマッピングすると思うんですが、これだとカスタムシリアライザーが大変なことになります([1敗](https://github.com/usbharu/Hideout/blob/1f2feb6668fe9e0d4c22ae0928315a329c9e56ad/hideout/hideout-activitypub/src/main/kotlin/dev/usbharu/hideout/activitypub/domain/model/ObjectDeserializer.kt))。ここまでしてもまだ完全にはデシリアライズできないんので、そもそも仕組みを変える必要がありそうです。
### JSONシリアライザに依存しすぎる問題
たとえ完璧なカスタムシリアライザーが出来上がったとしても、それはとんでもないクソデカコードでJSONシリアライザーの実装にべったり依存してるでしょう。新しく連合したいプロパティができるたびにカスタムシリアライザーをいじらないといけないなんてゴメンです。
### 独自プロパティ生やすのめんどくさすぎ問題
ActivityPub実装に使いたいので、独自のプロパティを簡単に生やせる機能は必須です。いま実際に送りつけあってるJSONみると何やねんこれ見たいなプロパティがいっぱい生えてます。[fediverse/fep: Fediverse Enhancement Proposals - Codeberg.org](https://codeberg.org/fediverse/fep)とか見ると「何お前?」みたいなのありますね。
### HTTP/HTTPS問題
`http://`から始まるURIと`https://`から始まるURIを区別しないでほしいんですが、普通のJSONシリアライザってそこまで器用なこと多分できないのでこれもカスタムシリアライザー作る必要があります。
## 実装
上の問題を全部簡単に解決する方法、それはもう~~TypeScriptを使う~~データの実体はMapに格納し、getter/setterでMapにアクセスするぐらいしか無いでしょう。Mapなら独自プロパティも格納し放題ですし、型のことも考えなくて済みます。これもうJavaScriptじゃね?
アノテーションと様々な黒魔術(リフレクション、動的プロキシ、動的バイトコード生成)を組み合わせて実現することも考えましたが、そのへんはよくわかんなかったので無理でした。そのうち勉強しときます。
```kotlin
interface JsonLd {
var json: JsonNode
val jsonObject: JsonObject
get() {
require(json.isObject)
return json as JsonObject
}
var type: List<String>
get() {
return jsonObject[Properties.TYPE]?.asArray()?.mapNotNull { it.asStringLiteralOrNull()?.value }
?: return emptyList()
}
set(value) {
return jsonObject.setOrRemove(Properties.TYPE, value.map { JsonString(it) }.toJsonArray())
}
var id: URI?
get() {
val string = jsonObject.obtain(ID)?.asStringLiteralOrNull() ?: return null
return URI.create(string.value)
}
set(value) {
jsonObject.setOrRemove(ID, value?.toString()?.let { JsonString(it) })
}
fun getAsObjectOrLink(id: String): List<ObjectOrLink> {
val jsonNode = jsonObject.obtain(id) ?: return emptyList()
return jsonNode.asArray().map { ObjectFactory.factory(it) as ObjectOrLink }
}
fun setAsObjectOrLink(id: String, `object`: List<ObjectOrLink>) {
jsonObject.setOrRemove(
id,
`object`.map { ObjectFactory.toJsonNode(it) }.toJsonArray()
)
}
}
```
これは全てのJSONの基底となるJsonLdクラスで、`jsonObject`がMapだと思ってOKです。こんな感じで生えているべきプロパティをinterfaceにgetter/setterとともに定義し、デフォルト実装を書いておきます。利用時は必要なインターフェースを全て実装したクラスを定義して、`ObjectFactory.factory(json)`で作成します。
```kotlin
inline fun <reified T : JsonLd> JsonLd.asTypeOfNull(type: String): T? {
if (this.type.contains(type).not()) {
return null
}
return this as? T
}
```
前述の通り`type`は配列なのでtypeに変換したい型が含まれていることを確認した後キャストします。
```kotlin
abstract class AbstractActivityStream : Activity, JsonLd, Object, ObjectOrLink, CollectionPage, Collection,
OrderedCollection, OrderedCollectionPage, Image, ImageOrLink, UriOrLink, Link, Note {
}
//このjsonはJsonLdインターフェースのjson
class DefaultActivityStream(override var json: JsonNode) : AbstractActivityStream() {
}
object ObjectFactory {
fun factory(jsonNode: JsonNode): JsonLd {
return DefaultActivityStream(jsonNode) //ここを独自プロパティを生やしたinterfaceを定義したクラスなどに差し替えることで対応する。
}
fun toJsonNode(jsonLd: JsonLd): JsonNode {
return jsonLd.json
}
}
```
このやる気のない`ObjectFactory`はとりあえず作ってそのまま放置されてるやつで、そのうち直します。
実装の方針としてListはnon-nullable、その他はnullableで定義しました。あとMutableListじゃなくてListで定義してますがMutableListでやっても行けそうなので今後変えていくかも。
## 現時点での使い方
```kotlin
@Test
fun name() {
val jsonElement = Json.parseToJsonElement(
"""
[
{
"https://www.w3.org/ns/activitystreams#content": [
{
"@value": "I am fine."
}
],
"@id": "http://www.test.example/notes/1",
"https://www.w3.org/ns/activitystreams#replies": [
{
"https://www.w3.org/ns/activitystreams#items": [
{
"https://www.w3.org/ns/activitystreams#content": [
{
"@value": "I am glad to hear it."
}
],
"https://www.w3.org/ns/activitystreams#inReplyTo": [
{
"@id": "http://www.test.example/notes/1"
}
],
"https://www.w3.org/ns/activitystreams#summary": [
{
"@value": "A response to the note"
}
],
"@type": [
"https://www.w3.org/ns/activitystreams#Note"
]
}
],
"https://www.w3.org/ns/activitystreams#totalItems": [
{
"@type": "http://www.w3.org/2001/XMLSchema#nonNegativeInteger",
"@value": 1
}
],
"@type": [
"https://www.w3.org/ns/activitystreams#Collection"
]
}
],
"https://www.w3.org/ns/activitystreams#summary": [
{
"@value": "A simple note"
}
],
"@type": [
"https://www.w3.org/ns/activitystreams#Note"
]
}
]
""".trimIndent()
)
val convert = KotlinxSerializationImpl.convert(jsonElement)
println(convert)
val factory = ObjectFactory.factory(convert.asArray()[0])
val note = factory.asTypeOfNull<Note>(Type.NOTE) ?: return
println(note.content.getAsMap()["default"])
println(note.summary)
println(note.id)
println(note.replies)
println(note.replies?.totalItems)
println(note.replies?.items)
note
.replies
?.items
.orEmpty()
.mapNotNull {
it.asTypeOfNull<Note>(Type.NOTE)
}
.map {
println(it.summary)
println(it.type)
println(it.content)
it.inReplyTo.map {
println(it.id)
}
}
}
```
### 出力例
println(note.content.getAsMap()["default"])
```
I am fine.
```
println(note.summary)
```
[LangString(language=null, value=A simple note)]
```
## 次に向けて
- デシリアライズがある程度できたのでシリアライズの実験もしたい
- `ObjectFactory`がうんこなのでなんとかしたい

View File

@ -1,100 +0,0 @@
---
author: usbharu
draft: false
categories:
- 技術
date: 2025-01-12T14:08:58+09:00
tags:
- Kotlin
- ActivityPub
- Kotlin
- ActivityStreams
- ActivityVocabulary
keywords:
- Kotlin
- ActivityPub
- Kotlin
- ActivityStreams
- ActivityVocabulary
title: ActivityStreamsをKotlinでいい感じに扱いたい(DSL編)
relpermalink: posts/2025-01-12/
url: posts/2025-01-12/
decription: ActivityStreamsをKotlinでいい感じに扱いたい(シリアライズ・DSL編)
---
前回([ActivityStreamsをKotlinでいい感じに扱いたい(デシリアライズ編) · usbharu - blog](https://blog.usbharu.dev/posts/2025-01-12/))デシリアライズの実装をした際にシリアライズの部分も作ってしまっていたのでDSLのついでに紹介します。
## シリアライズ
シリアライズというかマッピングのほうが正しいような気もする
usbharu/activity-streams-serialization at 00dc4f7939655fb9a1c4276016040821569e4f9a
https://github.com/usbharu/activity-streams-serialization/tree/00dc4f7939655fb9a1c4276016040821569e4f9a
(注) CollectionはKotlinのCollectionではなくて独自のCollection
```kotlin
var replies: Collection?
get() = replies()
set(value) {
jsonObject.setOrRemove(Properties.REPLIES, value?.json)
}
fun Object?.replies(objectFactory: ObjectFactory? = this?.objectFactory): Collection? {
if (this == null) {
return null
}
requireNotNull(objectFactory)
val jsonNode = jsonObject.obtain(Properties.REPLIES)?.asArray()?.firstOrNull() ?: return null
require(jsonNode.isObject)
return objectFactory.create(jsonNode) as Collection
}
```
シリアライズの方もデシリアライズと同じで、SetterでJsonNodeを直接弄っています。
## DSL
テストがすっげぇ面倒なのでDSLを作り始めました。
[DSL](https://github.com/usbharu/activity-streams-serialization/blob/00dc4f7939655fb9a1c4276016040821569e4f9a/src/test/kotlin/dev/usbharu/hideout/activitystreams/core/ObjectTest.kt#L77-L86)
```
val object1 = JsonLdBuilder().Note {
attachment {
listOf(
Image {
content("This is what he looks like.")
url("http://example.org/cat.jpeg")
})
}
name("Have you seen my cat?")
}
```
こんな感じでオブジェクトを組み立てることができます。attachmentのlistOfをなくせるように作ることも考えたんですが、仕組みが複雑になる割にメリット無いのでやめました。
KotlinでDSL作るのって簡単そうに見えて意外とややこしいですよね
### 実装
[オブジェクトの作成](https://github.com/usbharu/activity-streams-serialization/blob/00dc4f7939655fb9a1c4276016040821569e4f9a/src/main/kotlin/dev/usbharu/hideout/activitystreams/dsl/JsonLd.kt)
```kotlin
class JsonLdBuilder(var objectFactory: ObjectFactory = DefaultObjectFactory) {
fun Object(block: ObjectBuilder.() -> Unit = {}): Object {
val objectBuilder = ObjectBuilder(objectFactory, this)
objectBuilder.block()
return objectBuilder.Object
}
```
[プロパティの設定](https://github.com/usbharu/activity-streams-serialization/blob/00dc4f7939655fb9a1c4276016040821569e4f9a/src/main/kotlin/dev/usbharu/hideout/activitystreams/dsl/ObjectBuilder.kt)
```kotlin
fun attachment(objectOrLink: ObjectOrLink?): ObjectBuilder {
Object.attachment += objectOrLink ?: return this
return this
}
```

View File

@ -1,101 +0,0 @@
---
author: usbharu
draft: false
categories:
- 技術
date: 2025-01-27T14:08:58+09:00
tags:
- Kotlin
- ActivityPub
- Kotlin
- ActivityStreams
- ActivityVocabulary
keywords:
- Kotlin
- ActivityPub
- Kotlin
- ActivityStreams
- ActivityVocabulary
title: Kotlinで作るDSL
relpermalink: posts/2025-01-27/
url: posts/2025-01-27/
decription: KotlinでデータとDSLが分離されたタイプのDSLの作成
---
[usbharu/kotlin-dsl-demo](https://github.com/usbharu/kotlin-dsl-demo)
KotlinでひたすらDSLを書いてたらだいぶウハウが溜まってきたので共有
KotlinのDSL作成でググるとこんな記事が出てくると思いますが、大半の人は下の記事を読むだけで作りたいものが作れると思うのでぜひ読んでください。
[Kotlin で書く DSL](https://improve-future.com/kotlin-dsl.html)
[Type-safe builders | Kotlin Documentation](https://kotlinlang.org/docs/type-safe-builders.html)
## データの実体とDSLが分離されたタイプのDSL
上で紹介した記事では全部データの実体となるクラスに直接DSLを生やしています。これはこれで綺麗にまとまっていいのかもしれませんが、僕は頭Javaな人なのでBuilderに相当する部分は分けられていてほしいです。特に内部でStringBuilderなどを使うようになってくるとややこしくなってきます。
分離すると何が嬉しいかというと
- DSL上の構造と実際のデータ構造が大幅に乖離しているときにDSLを書きやすい
- StringBuilderなどが使いやすい
- データ保持側に手を入れなくていい(責務の分離)
- クソややこしいDSLを書いたときにデータ保持側を読みやすい
- パフォーマンスが向上する場合がある(とりあえずListに溜め込んでbuild時に生成するStringBuilder的なアプローチがとれる)
逆にデメリットは
- 単純に考えて必要なクラス数が増える(どこまで凝るかによるが2倍以上)
- それに伴う保守性の低下
- プロパティの露出
### メリット
#### DSL上の構造と実際のデータ構造が大幅に乖離しているときにDSLを書きやすい
実際のデータにDSLを生やした場合全然関係ないインスタンスを操作することは可能ですが不自然です。しかし、DSL部分を分離するとそのへんの制約は完全になくなって自由になります。
#### StringBuilderなどが使いやすい
上と同じで、データ側がStringBuilderをもともと持っているなら自然に使えますが、持ってない場合StringBuilderの意味がなくなります。
#### データ保持側に手を入れなくていい
そう、責務の分離ってやつです。わざわざ説明するまでもない
#### クソややこしいDSLを書いたときにデータ保持側を読みやすい
例えば[一つのオブジェクトのDSLに300行以上あったら](https://github.com/usbharu/activity-streams-serialization/blob/6e34c1ce3f2d070396c908f139b0863312262c45/src/main/kotlin/dev/usbharu/activitystreamsserialization/dsl/ObjectBuilder.kt)すごいややこしくないですか? そういうときに読みやすいです。
#### パフォーマンスが向上する場合がある
StringBuilderがいい例なんですが、一旦Listに溜め込んで、build時にインスタンスの生成するっていうアプローチを取れます。
これはKotlin標準のDSL
```kotlin
val buildString = buildString {
append("hello")
append(" ")
append("world")
for (i in 1..10) {
append(i)
}
}
```
### デメリット
#### 必要なクラス数が増える & 保守性の低下
データのクラスとそれに対応するBuilderというかDSLの分増えるので雑に計算すると2倍です。共通化できる部分もあるのでもうちょっと少ない場合もあるかも。それに伴って保守性も低下します。
#### プロパティの露出
デフォルト実装作ろうとするとinterfaceはprotectedにできないしpackage-privateも無いのでpublicにせざるを得ず、触ってほしくないプロパティまで露出してしまいます。一応internalはありますが、プロジェクト内で使うDSLとかだと微妙だし~~[そもそもinternalは無視できる](https://stackoverflow.com/questions/62500464/is-it-possible-to-access-internal-class-with-reflection-in-kotlin)~~
というわけで色々実践してみたリポジトリ
[usbharu/kotlin-dsl-demo](https://github.com/usbharu/kotlin-dsl-demo)