第3章:Expression文法編

expr-lang/expr - runnの式評価エンジン

runnは式評価エンジンとしてexpr-lang/exprを採用しています。これにより、柔軟で強力な式評価が可能になります。

expr-lang/exprの特徴

  • Go風の構文
  • 型安全性
  • 高速な実行速度
  • サンドボックス環境での安全な実行

基本的な式の構文

リテラルと演算子

expr-lang/exprでは、さまざまなリテラルと演算子を使ってデータを表現できます。詳細はexpr-lang Literals(リテラル)を参照してください。

種類 説明
コメント // コメント/* コメント */ 単一行コメント
ブール値 truefalse 真偽値
整数 42-37, 0x2A, 0o52, 0b101010 整数値
浮動小数点数 0.5.5 小数値
文字列 "Hello"'World' 文字列
配列 [1, 2, 3] 配列・リスト
マップ {a: 1, b: 2, c: 3} 連想配列・オブジェクト
nil nil null値
steps:
  literals_demo:
    test: |
      // 数値
      42 == 42 &&
      3.14 < 4 &&

      // 文字列
      "hello" + " world" == 'hello world' &&

      // ブール値
      true && !false &&

      // 配列
      [1, 2, 3][0] == 1 &&
      len([1, 2, 3]) == 3 &&

      // マップ
      {"name": "alice", "age": 30}.name == "alice"

比較演算子

steps:
  comparison_demo:
    test: |
      // 基本的な比較
      10 > 5 &&
      "apple" < "banana" &&
      100 >= 100 &&
      50 <= 100 &&

      // 等価性
      "test" == "test" &&
      100 != 99 &&

      // 包含チェック
      "running" contains "run" &&
      2 in [1, 2, 3] &&
      "key" in {"key": "value"}

変数参照の詳細

利用可能な変数一覧

変数名 スコープ 説明
vars グローバル Runbookで定義された変数
env グローバル 環境変数
steps グローバル すべてのステップの結果
current ステップ内 現在のステップの結果
previous ステップ内 直前のステップの結果
i ループ内 ループのインデックス
parent Include内 親Runbookの変数

変数アクセスの実践例

desc: 変数参照の包括的な例

vars:
  baseURL: https://api.example.com
  users:
    - id: 1
      name: Alice
    - id: 2
      name: Bob

runners:
  blog: http://localhost:8080

steps:
  # varsへのアクセス
  access_vars:
    dump: |
      {
        "url": vars.baseURL,
        "firstUser": vars.users[0].name,
        "userCount": len(vars.users)
      }

  # 環境変数へのアクセス
  access_env:
    test: |
      env.HOME != "" &&
      env.USER != ""

  # ステップ結果へのアクセス(マップ形式)
  make_request:
    blog:
      /users:
        get: {}
    test: current.res.status == 200
    dump: current.res.body

  # 前のステップの結果を参照
  use_previous:
    test: |
      previous.res.status == 200 &&
      len(steps.make_request.res.body) >= 0

高度な式パターン

条件式(三項演算子)

steps:
  conditional_expr:
    dump: |
      // 三項演算子
      vars.environment == "prod" ? "https://api.example.com" : "http://localhost:8080"

フィルタリングとマッピング

vars:
  products:
    - name: "iPhone"
      price: 999
      category: "electronics"
    - name: "Book"
      price: 20
      category: "books"
    - name: "MacBook"
      price: 1999
      category: "electronics"
steps:
  filter_example:
    dump: |
      // 価格が100以上の商品をフィルタ
      filter(vars.products, {.price >= 100})

    test: |
      // カテゴリが"electronics"の商品数をカウント
      len(filter(vars.products, {.category == "electronics"})) == 2

  map_example:
    dump: |
      // 商品名のリストを作成
      map(vars.products, {.name})

    test: |
      // すべての商品の価格が0より大きいことを確認
      all(vars.products, {.price > 0})

配列・マップ操作

vars:
  numbers: [1, 2, 3, 4, 5]
  person:
    name: "Alice"
    skills:
      - "Go"
      - "Python"
      - "JavaScript"

steps:
  array_operations:
    test: |
      // スライス操作
      vars.numbers[1:3] == [2, 3] &&
      vars.numbers[:2] == [1, 2] &&
      vars.numbers[3:] == [4, 5] &&

      // 要素の存在確認
      3 in vars.numbers &&
      !(10 in vars.numbers)

      // 配列の結合(TODO: これは動かない)
      // vars.numbers + [6, 7] == [1, 2, 3, 4, 5, 6, 7]

  map_operations:
    test: |
      // ネストしたアクセス
      vars.person.skills[0] == "Go" &&
      len(vars.person.skills) == 3 &&

      // キーの存在確認
      "name" in vars.person &&
      !("age" in vars.person)

実践的な式の例

APIレスポンスの検証

desc: 複雑なAPIレスポンスの検証

runners:
  blog: http://localhost:8080

steps:
  add_user:
    blog:
      /users:
        post:
          body:
            application/json:
              name: "John Doe"
              email: "john@example.com"

  get_users:
    blog:
      /users:
        get:
          query:
            page: 1
            limit: 10
    test: |
      // ステータスコードの確認
      current.res.status == 200 &&

      // レスポンスボディの構造確認
      // "data" in current.res.body &&
      // "pagination" in current.res.body &&

      // データの検証
      len(current.res.body) <= 10
      // claude が以下のようなコードを生成するが実際には動かない
      // all(current.res.body.data, {
      //   "id" in . &&
      //   "email" in . &&
      //   .id > 0
      // }) &&

      // ページネーションの検証
      // current.res.body.pagination.page == 1 &&
      // current.res.body.pagination.limit == 10

デバッグのテクニック

dump機能の活用

TBD

よくあるパターンと落とし穴

1. null/undefinedの扱い

steps:
  null_handling:
    test: |
      # nullチェック
      current.res.body.optional_field != null &&

      # デフォルト値の設定
      (current.res.body.optional_field ?? "default") != "default" &&

      # ネストしたnullチェック
      current.res.body.user?.profile?.bio != null

2. 型変換

steps:
  type_conversion:
    test: |
      # 文字列から数値への変換は自動では行われない
      current.res.body.count == "10" &&  # 文字列として比較
      int(current.res.body.count) == 10  # 数値として比較

3. 配列の境界チェック

steps:
  safe_array_access:
    test: |
      # 配列が空でないことを確認してからアクセス
      len(current.res.body.items) > 0 &&
      current.res.body.items[0].name == "test"