はじめに

編集

JSLTは、JSONデータの変換とクエリを行うための専用言語として開発されました。本ハンドブックでは、JSLTの基礎から実践的な使用方法まで、具体的なコード例を交えながら解説していきます。

基本概念

編集

JSONデータの操作において、データの変換やフィルタリングは頻繁に必要となる作業です。JSLTはこれらの作業を効率的に行うための専用言語として設計されており、jq、XPath、XQueryからインスピレーションを得ています。以下に基本的な変換例を示します:

// 入力JSONデータ
{
  "user": {
    "name": "John Smith",
    "age": 28,
    "email": "john@example.com",
    "preferences": {
      "theme": "dark",
      "notifications": true
    }
  }
}

// JSLT変換
{
  "profile": {
    "fullName": .user.name,
    "contactInfo": .user.email,
    "settings": .user.preferences,
    "isAdult": .user.age >= 20
  }
}

データアクセスと変換

編集

JSLTにおけるデータアクセスは、ドット記法を基本としています。複雑なデータ構造に対しても、直感的なアクセスが可能です:

// ネストされたオブジェクトの変換例
{
  "userDetails": {
    "personal": {
      "name": .user.profile.name,
      "age": .user.profile.age,
      "formattedName": string(.user.profile.firstName) + " " + string(.user.profile.lastName)
    },
    "contact": {
      "primaryEmail": .user.contact.email[0],
      "phoneNumbers": [for (.user.contact.phones) {
        "number": .,
        "formatted": format-phone(.)
      }]
    }
  }
}

条件付き変換

編集

実際のデータ変換では、条件に基づいて異なる出力を生成する必要があります。JSLTは豊富な条件式をサポートしています:

{
  "account": {
    "status": if (.user.age < 20) 
              "junior" 
              else if (.user.age < 60) 
              "regular" 
              else 
              "senior",
    "features": {
      "canPost": .user.age >= 13,
      "canModerate": .user.age >= 18 and .user.reputation > 100,
      "privileges": [
        if (.user.isPremium) "premium-support",
        if (.user.isVerified) "verified-badge",
        if (.user.contributions > 1000) "power-user"
      ]
    }
  }
}

配列とコレクションの処理

編集

配列データの処理は、多くのデータ変換タスクで重要な部分を占めます。JSLTは配列に対する強力な操作機能を提供します:

// 入力データ
{
  "items": [
    {"id": 1, "name": "Item 1", "price": 100},
    {"id": 2, "name": "Item 2", "price": 200},
    {"id": 3, "name": "Item 3", "price": 300}
  ]
}

// 変換処理
{
  "summary": {
    "totalItems": size(.items),
    "totalValue": sum([for (.items) .price]),
    "formattedItems": [
      for (.items) {
        "displayName": .name + " (#" + string(.id) + ")",
        "priceWithTax": .price * 1.1,
        "category": if (.price < 150) 
                   "budget" 
                   else if (.price < 250) 
                   "standard" 
                   else 
                   "premium"
      }
    ],
    "priceRanges": {
      "under150": [for (.items) if (.price < 150) .name],
      "under250": [for (.items) if (.price >= 150 and .price < 250) .name],
      "above250": [for (.items) if (.price >= 250) .name]
    }
  }
}

カスタム関数と変数

編集

複雑な変換を整理するために、JSLTではカスタム関数と変数を定義できます:

def calculate-discount(price, membership-level) {
  if ($membership-level == "premium")
    $price * 0.8
  else if ($membership-level == "regular")
    $price * 0.9
  else
    $price
}

def format-currency(amount) {
  round($amount * 100) / 100
}

let tax-rate = 0.1
let shipping-base = 500

{
  "order": {
    "items": [
      for (.cart.items) {
        "product": .name,
        "originalPrice": .price,
        "discountedPrice": format-currency(
          calculate-discount(.price, .user.membership)
        ),
        "finalPrice": format-currency(
          calculate-discount(.price, .user.membership) * (1 + $tax-rate)
        )
      }
    ],
    "summary": {
      "subtotal": sum([for (.cart.items) .price]),
      "tax": sum([for (.cart.items) .price]) * $tax-rate,
      "shipping": if(sum([for (.cart.items) .price]) > 5000)
                 0
                 else
                 $shipping-base,
      "total": format-currency(
        sum([for (.cart.items) 
          calculate-discount(.price, .user.membership)
        ]) * (1 + $tax-rate)
      )
    }
  }
}

オブジェクトのマージと変換

編集

複数のオブジェクトを組み合わせて新しいオブジェクトを作成する場合、JSLTは柔軟な方法を提供します:

// 入力データ
{
  "user": {
    "profile": {
      "name": "John Smith",
      "age": 28
    },
    "settings": {
      "theme": "dark",
      "language": "en"
    }
  },
  "metadata": {
    "lastUpdated": "2024-01-01",
    "version": "1.0"
  }
}

// 変換処理
{
  // 基本プロファイル情報をコピー
  * : .user.profile,
  
  // 設定情報を settings キーの下にネスト
  "settings": {
    * : .user.settings,
    "updatedAt": .metadata.lastUpdated
  },
  
  // 追加の計算済みフィールド
  "category": if (.user.profile.age < 20) 
              "youth" 
              else 
              "adult",
              
  // メタデータの選択的なコピー
  "system": {
    "version": .metadata.version,
    "generatedAt": now()
  }
}

エラー処理とバリデーション

編集

データ変換時には、入力データの検証やエラー処理が重要です。JSLTでは以下のように実装できます:

def validate-email(email) {
  contains($email, "@") and contains($email, ".")
}

def validate-age(age) {
  $age >= 0 and $age < 150
}

{
  "validation": {
    "isValid": all([
      validate-email(.user.email),
      validate-age(.user.age),
      size(.user.name) > 0
    ]),
    "errors": [
      if (not(validate-email(.user.email)))
        "Invalid email format",
      if (not(validate-age(.user.age)))
        "Age out of valid range",
      if (size(.user.name) == 0)
        "Name is required"
    ]
  },
  
  // バリデーション結果に基づいて条件付きで出力を生成
  "processedData": if(validate-email(.user.email) and 
                     validate-age(.user.age) and 
                     size(.user.name) > 0) {
    "profile": {
      * : .user,
      "verified": true,
      "processingDate": now()
    }
  } else null
}

高度なデータ変換パターン

編集

複雑なビジネスロジックを実装する際の実践的なパターンを示します:

def calculate-tier(points) {
  if ($points >= 10000)
    "platinum"
  else if ($points >= 5000)
    "gold"
  else if ($points >= 1000)
    "silver"
  else
    "bronze"
}

def format-date(timestamp) {
  format-time(
    parse-time($timestamp, "yyyy-MM-dd'T'HH:mm:ssX"),
    "MMMM d, yyyy"
  )
}

{
  "memberAnalysis": {
    "overview": {
      "joinDate": format-date(.member.joinDate),
      "currentTier": calculate-tier(.member.points),
      "nextTier": calculate-tier(.member.points + 1000),
      "pointsToNextTier": 
        if (.member.points >= 10000)
          0
        else if (.member.points >= 5000)
          10000 - .member.points
        else if (.member.points >= 1000)
          5000 - .member.points
        else
          1000 - .member.points
    },
    
    "transactions": [
      for (.member.transactions) {
        "date": format-date(.timestamp),
        "amount": round(.amount * 100) / 100,
        "points": round(.amount * 10),
        "type": .type,
        "status": if (.processed)
                 "completed"
                 else if (.pending)
                 "pending"
                 else
                 "failed"
      }
    ],
    
    "recommendations": {
      "products": [
        for (.catalog.items)
        if (.category in .member.interests and
            .price <= .member.averageSpend * 1.2)
        {
          "id": .id,
          "name": .name,
          "price": .price,
          "matchScore": size(
            [for (.tags) 
             if (. in .member.preferences) .]
          ) * 10
        }
      ]
    }
  }
}

附録:性能最適化のベストプラクティス

編集

JSLTでの変換を最適化する際の重要な考慮点を説明します。以下のコードは、最適化前と最適化後の比較を示しています:

// 最適化前の実装
{
  "items": [
    for (.products) {
      "name": .name,
      "price": .price,
      // 毎回同じ計算を繰り返している
      "taxRate": if (.category == "food") 0.08 else 0.1,
      "priceWithTax": .price * (if (.category == "food") 0.08 else 0.1)
    }
  ]
}

// 最適化後の実装
let food-tax = 0.08
let standard-tax = 0.1

def get-tax-rate(category) {
  if ($category == "food") 
    $food-tax 
  else 
    $standard-tax
}

{
  "items": [
    for (.products) let tax-rate = get-tax-rate(.category) {
      "name": .name,
      "price": .price,
      "taxRate": $tax-rate,
      "priceWithTax": .price * (1 + $tax-rate)
    }
  ]
}

このハンドブックは、実践的なJSLT開発の指針として活用できます。各セクションのコード例は、実際のユースケースに基づいており、必要に応じて修正して使用することができます。