AWS Step Functionsの状態JSONの操作

## この記事について

[AWS Step Functions][states]はAWS上に構築されたシステムの運用を自動化する際にとても便利な、一種のワークフローエンジンです。様々な利点があるサービスですが、ワークフロー構築に際しては、単一のJSON構造体(状態JSON)の管理がユーザーに任されており、その状態JSONを操作する方法が非常に限定的[^1]である点が筆者には難しく感じられました。

[states]: https://aws.amazon.com/jp/step-functions/
[^1]: Lambdaを呼び出せばプログラミング言語で行える操作はすべて行えますが、ここではStep Functionsの機能のみで見ると極めて限定的であることを言っています。

この記事では、「プログラミング言語でのJSONの基本的な操作をStep Functionsで実現する方法(コード)」を紹介します。

## おことわり

AWS Step Functionsが何か、どのように使うのかといった詳細は[公式ドキュメント][docs-states]などをご覧ください。

[docs-states]: https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/welcome.html

紹介するJSON操作は比較的複雑なので多用する場合はLambdaとの併用などを検討するべきだと筆者は考えます。

紹介しているStateMachine定義は、わかりやすさのため、入力チェックなどは実装していません。

## 本文

### 配列への要素の(末尾への)追加

JavaScriptやPythonでいう以下のような処理です:

```js
const value = 3; // 値は何でもよい
const array = [1, 2, 4, 5]; // 配列も何でもよい
array.push(value); // この操作
```

```py
value = 3  # 値は何でもよい
array = [1, 2, 4, 5]  # 配列も何でもよい
array.append(3)  # この操作
```

StateMachine定義:

```json
{
  "StartAt": "NestArray",
  "States": {
    "NestArray": {
      "Type": "Pass",
      "Parameters": {
        "value.$": "$.value",
        "array.$": "$.array",
        "nested.$": "States.Array($.array, States.Array($.value))"
      },
      "Next": "Flatten"
    },
    "Flatten": {
      "Type": "Pass",
      "End": true,
      "Parameters": {
        "value.$": "$.value",
        "array.$": "$.array",
        "concatenated.$": "$.nested[*][*]"
      }
    }
  }
}
```

見てわかる通り、後ろに追加するのが単一要素である理由はないので、配列の連結も似た形で行えます。


### 連想配列へのキー/値ペアの追加

JS/Pythonでの例:

```js
// 任意のキー/値ペア(キーは文字列。値は任意のJSON)
const k = 'key';
const v = 'val';
// 任意の連想配列(オブジェクト)
const m = {
  'arbitrary': 'content',
};
m[k] = v; // この処理
```

```py
# 任意のキー/値ペア(キーは文字列。値は任意のJSON)
k = 'key'
v = 'val'
m = {
  'arbitrary': 'content',
}
m[k] = v  # この処理
```

StateMachine定義:

```json
{
  "StartAt": "CreateEntry",
  "States": {
    "CreateEntry": {
      "Type": "Pass",
      "Parameters": {
        "key.$": "$.key",
        "value.$": "$.value",
        "map.$": "$.map",
        "entry.$": "States.StringToJson(States.Format('\\{\"{}\":{}\\}', $.key, States.JsonToString($.value)))"
      },
      "Next": "PutEntry"
    },
    "PutEntry": {
      "Type": "Pass",
      "End": true,
      "Parameters": {
        "key.$": "$.key",
        "value.$": "$.value",
        "map.$": "$.map",
        "result.$": "States.JsonMerge($.map, $.entry, false)"
      }
    }
  }
}
```


### 配列の任意の位置への挿入

JS/Pythonでの例:

```js
const value = 3; // 挿入する要素の値
const pos = 2; // 要素を挿入する位置(index)
const array = [1, 2, 4, 5]; // 要素を挿入する配列
array.splice(pos, 0, value); // この処理
```

```py
value = 3  # 挿入する要素の値
pos = 2  # 要素を挿入する位置(index)
array = [1, 2, 4, 5]  # 要素を挿入する配列
array.insert(pod, value)  # この処理
```

StateMachine定義:

```json
{
  "StartAt": "Chunk",
  "States": {
    "Chunk": {
      "Type": "Pass",
      "Next": "Slice",
      "Parameters": {
        "valueInserted.$": "$.valueInserted",
        "insertPosition.$": "$.insertPosition",
        "array.$": "$.array",
        "chunked.$": "States.ArrayPartition($.array, $.insertPosition)"
      }
    },
    "Slice": {
      "Type": "Pass",
      "Next": "Nest",
      "Parameters": {
        "valueInserted.$": "$.valueInserted",
        "insertPosition.$": "$.insertPosition",
        "array.$": "$.array",
        "arrayHead.$": "$.chunked[0]",
        "arrayTail.$": "$.chunked[1:][*]",
        "arrayInserted.$": "States.Array($.valueInserted)"
      }
    },
    "Nest": {
      "Type": "Pass",
      "Parameters": {
        "valueInserted.$": "$.valueInserted",
        "insertPosition.$": "$.insertPosition",
        "array.$": "$.array",
        "nested.$": "States.Array($.arrayHead, $.arrayInserted, $.arrayTail)"
      },
      "Next": "Flat"
    },
    "Flat": {
      "Type": "Pass",
      "End": true,
      "Parameters": {
        "valueInserted.$": "$.valueInserted",
        "insertPosition.$": "$.insertPosition",
        "array.$": "$.array",
        "inserted.$": "$.nested[*][*]"
      }
    }
  }
}
```