effect-tsで列挙子や定数などの変換

よくわからないエラー定数などを変換するためにSchemaを無理やり使ってみたメモ

# 参考

https://effect.website/docs/schema/transformations/#combining-two-primitive-schemas

https://developer.mozilla.org/ja/docs/Web/HTTP/Reference/Status

# httpステータスコードによる変換

object <=> string などのような変換をするときに変数名を覚えるのではなく、decoderを使い分けるようにするとよいのでは？

```plantuml
@startuml
object "100" as codes 

map "人間用データ定義" as struct {
 status_code => 100
 status_code_text => Continue
 overview => これは暫定レスポンスで、その時点までのすべてに ...
 isStandard => true
}

struct -> codes : encode
codes -> struct : decode
@enduml
```

```ts
import { Schema } from 'effect'
import { describe, it } from 'vitest'

const status_codes = Schema.Literal(100, 200, 300, 400, 500, 999)
const status_codes_text = Schema.Literal(
  'Continue',
  'OK',
  'Multiple Choices',
  'Bad Request',
  'Internal Server Error',
  '標準外レスポンス'
)
type type_status_codes = Schema.Schema.Type<typeof status_codes>
type type_status_codes_text = Schema.Schema.Type<typeof status_codes_text>
const status_codes_set = Schema.Union(status_codes, status_codes_text)

const status_codes_for_human_being_standard = Schema.Struct({
  status_code: status_codes,
  status_code_text: status_codes_text,
  overview: Schema.String,
  isStandard: Schema.Literal(true)
})
const status_codes_for_human_being_standard_not_standard = Schema.Struct({
  status_code: status_codes,
  status_code_text: status_codes_text,
  overview: Schema.String,
  isStandard: Schema.Literal(false),
  registrationDate: Schema.DateFromSelf
})
const status_codes_for_human_being = Schema.Union(
  status_codes_for_human_being_standard_not_standard,
  status_codes_for_human_being_standard
)
type type_status_codes_for_human_being = Schema.Schema.Type<typeof status_codes_for_human_being>

const _100_Continue: type_status_codes_for_human_being = {
  status_code: 100,
  status_code_text: 'Continue',
  overview:
    'これは暫定レスポンスで、その時点までのすべてに問題がなく、クライアントはリクエストを継続してよい、またもしリクエストが完了している場合はレスポンスを無視してよいことを示します。',
  isStandard: true
}

const _200_OK: type_status_codes_for_human_being = {
  status_code: 200,
  status_code_text: 'OK',
  overview: `
    リクエストが成功したことを示します。成功が意味することは、 HTTP メソッドにより異なります。
      * GET: リソースが読み込まれ、メッセージ本文で転送された。
      * HEAD: メッセージ本文がなく、表現ヘッダーがレスポンスに含まれている。
      * PUT または POST: 操作の結果を表すリソースがメッセージ本文で送信される。
      * TRACE: メッセージ本文に、サーバーが受け取ったリクエストメッセージが含まれている。
    `,
  isStandard: true
}

const _300_Multiple_Choices: type_status_codes_for_human_being = {
  status_code: 300,
  status_code_text: 'Multiple Choices',
  overview:
    'リクエストに対して複数のレスポンスがあることを示します。ユーザーエージェントやユーザーは、それらからひとつを選択します。 (複数のレスポンスからひとつを選ぶ方法は標準化されていませんが、選択肢へリンクする HTML が推奨されており、それによってユーザーが選択することができます。)',
  isStandard: true
}

const _400_Bad_Request: type_status_codes_for_human_being = {
  status_code: 400,
  status_code_text: 'Bad Request',
  overview:
    'クライアントのエラーとみなされるもの（例えば、不正なリクエスト構文、不正なリクエストメッセージフレーム、不正なリクエストルーティング）のために、 サーバーがリクエストを処理できない、あるいは処理しようとしない場合を示します。',
  isStandard: true
}

const _500_Internal_Server_Error: type_status_codes_for_human_being = {
  status_code: 500,
  status_code_text: 'Internal Server Error',
  overview: 'サーバー側で処理方法がわからない事態が発生したことを示します。',
  isStandard: true
}

const _999_標準外レスポンス: type_status_codes_for_human_being = {
  status_code: 999,
  status_code_text: '標準外レスポンス',
  overview: '誰か追加したのか分からないレスポンス',
  isStandard: false,
  registrationDate: new Date(2025, 2, 21)
}

function get_data_from_status_codes(code: type_status_codes | type_status_codes_text) {
  switch (code) {
    case 100:
    case 'Continue':
      return _100_Continue
    case 200:
    case 'OK':
      return _200_OK
    case 300:
    case 'Multiple Choices':
      return _300_Multiple_Choices
    case 400:
    case 'Bad Request':
      return _400_Bad_Request
    case 500:
    case 'Internal Server Error':
      return _500_Internal_Server_Error
    case 999:
    case '標準外レスポンス':
      return _999_標準外レスポンス
    default: {
      throw new Error(`error status_codes: ${code}`)
    }
  }
}

const StructFromStatusCode = Schema.transform(status_codes_set, status_codes_for_human_being, {
  strict: true,
  decode: (code) => get_data_from_status_codes(code) as any,
  encode: (struct) => struct.status_code
})
const decodeForHuman = Schema.decodeEither(StructFromStatusCode)
const encodeToStatusCode = Schema.encodeEither(StructFromStatusCode)

describe('Schema Transformations サンプルOK', () => {
  it('httpレスポンスステータスコード 人間用への変換1', () => {
    const struct = decodeForHuman(100)
    console.log(struct)
  })
  it('httpレスポンスステータスコード 人間用への変換2', () => {
    const struct = decodeForHuman('OK')
    console.log(struct)
  })
  it('httpレスポンスステータスコード 人間用への変換3', () => {
    const struct = decodeForHuman(999)
    console.log(struct)
  })
  it('httpレスポンスステータスコード コードへの変換', () => {
    const code = encodeToStatusCode(_400_Bad_Request)
    console.log(code)
  })
})

describe('Schema Transformations サンプルNG', () => {
  it('httpレスポンスステータスコード 人間用への変換NG 1', () => {
    const struct = decodeForHuman(123 as any)
    console.log(struct)
  })
  it('httpレスポンスステータスコード 人間用への変換NG 2', () => {
    const struct = decodeForHuman('NG' as any)
    console.log(struct)
  })
  it('httpレスポンスステータスコード コードへの変換NG', () => {
    const code = encodeToStatusCode({ test: 123 } as any)
    console.log(code)
  })
})
```

実行結果OKの場合
```ts
{
  _id: 'Either',
  _tag: 'Right',
  right: {
    status_code: 100,
    status_code_text: 'Continue',
    overview: 'これは暫定レスポンスで、その時点までのすべてに問題がなく、クライアントはリクエストを継続してよい、またもしリクエストが完了している場合はレスポンスを無視してよいことを示します。',
    isStandard: true
  }
}
{
  _id: 'Either',
  _tag: 'Right',
  right: {
    status_code: 200,
    status_code_text: 'OK',
    overview: '\n' +
      '    リクエストが成功したことを示します。成功が意味することは、 HTTP メソッドにより異なります。\n' +
      '      * GET: リソースが読み込まれ、メッセージ本文で転送された。\n' +
      '      * HEAD: メッセージ本文がなく、表現ヘッダーがレスポンスに含まれている。\n' +
      '      * PUT または POST: 操作の結果を表すリソースがメッセージ本文で送信される。\n' +
      '      * TRACE: メッセージ本文に、サーバーが受け取ったリクエストメッセージが含まれている。\n' +
      '    ',
    isStandard: true
  }
}
{
  _id: 'Either',
  _tag: 'Right',
  right: {
    status_code: 999,
    status_code_text: '標準外レスポンス',
    overview: '誰か追加したのか分からないレスポンス',
    isStandard: false,
    registrationDate: 2025-03-20T15:00:00.000Z
  }
}
{ _id: 'Either', _tag: 'Right', right: 400 }
```

実行結果NGの場合
```ts
{
  _id: 'Either',
  _tag: 'Left',
  left: {
    _id: 'ParseError',
    message: '(100 | 200 | 300 | 400 | 500 | 999 | "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス" <-> { readonly status_code: 100 | 200 | 300 | 400 | 500 | 999; readonly status_code_text: "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"; readonly overview: string; readonly isStandard: false; readonly registrationDate: DateFromSelf } | { readonly status_code: 100 | 200 | 300 | 400 | 500 | 999; readonly status_code_text: "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"; readonly overview: string; readonly isStandard: true })\n' +
      '└─ Encoded side transformation failure\n' +
      '   └─ 100 | 200 | 300 | 400 | 500 | 999 | "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"\n' +
      '      ├─ 100 | 200 | 300 | 400 | 500 | 999\n' +
      '      │  ├─ Expected 100, actual 123\n' +
      '      │  ├─ Expected 200, actual 123\n' +
      '      │  ├─ Expected 300, actual 123\n' +
      '      │  ├─ Expected 400, actual 123\n' +
      '      │  ├─ Expected 500, actual 123\n' +
      '      │  └─ Expected 999, actual 123\n' +
      '      └─ "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"\n' +
      '         ├─ Expected "Continue", actual 123\n' +
      '         ├─ Expected "OK", actual 123\n' +
      '         ├─ Expected "Multiple Choices", actual 123\n' +
      '         ├─ Expected "Bad Request", actual 123\n' +
      '         ├─ Expected "Internal Server Error", actual 123\n' +
      '         └─ Expected "標準外レスポンス", actual 123'
  }
}
{
  _id: 'Either',
  _tag: 'Left',
  left: {
    _id: 'ParseError',
    message: '(100 | 200 | 300 | 400 | 500 | 999 | "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス" <-> { readonly status_code: 100 | 200 | 300 | 400 | 500 | 999; readonly status_code_text: "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"; readonly overview: string; readonly isStandard: false; readonly registrationDate: DateFromSelf } | { readonly status_code: 100 | 200 | 300 | 400 | 500 | 999; readonly status_code_text: "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"; readonly overview: string; readonly isStandard: true })\n' +
      '└─ Encoded side transformation failure\n' +
      '   └─ 100 | 200 | 300 | 400 | 500 | 999 | "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"\n' +
      '      ├─ 100 | 200 | 300 | 400 | 500 | 999\n' +
      '      │  ├─ Expected 100, actual "NG"\n' +
      '      │  ├─ Expected 200, actual "NG"\n' +
      '      │  ├─ Expected 300, actual "NG"\n' +
      '      │  ├─ Expected 400, actual "NG"\n' +
      '      │  ├─ Expected 500, actual "NG"\n' +
      '      │  └─ Expected 999, actual "NG"\n' +
      '      └─ "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"\n' +
      '         ├─ Expected "Continue", actual "NG"\n' +
      '         ├─ Expected "OK", actual "NG"\n' +
      '         ├─ Expected "Multiple Choices", actual "NG"\n' +
      '         ├─ Expected "Bad Request", actual "NG"\n' +
      '         ├─ Expected "Internal Server Error", actual "NG"\n' +
      '         └─ Expected "標準外レスポンス", actual "NG"'
  }
}
{
  _id: 'Either',
  _tag: 'Left',
  left: {
    _id: 'ParseError',
    message: '(100 | 200 | 300 | 400 | 500 | 999 | "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス" <-> { readonly status_code: 100 | 200 | 300 | 400 | 500 | 999; readonly status_code_text: "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"; readonly overview: string; readonly isStandard: false; readonly registrationDate: DateFromSelf } | { readonly status_code: 100 | 200 | 300 | 400 | 500 | 999; readonly status_code_text: "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"; readonly overview: string; readonly isStandard: true })\n' +
      '└─ Type side transformation failure\n' +
      '   └─ { readonly status_code: 100 | 200 | 300 | 400 | 500 | 999; readonly status_code_text: "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"; readonly overview: string; readonly isStandard: false; readonly registrationDate: DateFromSelf } | { readonly status_code: 100 | 200 | 300 | 400 | 500 | 999; readonly status_code_text: "Continue" | "OK" | "Multiple Choices" | "Bad Request" | "Internal Server Error" | "標準外レスポンス"; readonly overview: string; readonly isStandard: true }\n' +
      '      └─ { readonly isStandard: false | true }\n' +
      '         └─ ["isStandard"]\n' +
      '            └─ is missing'
  }
}
```