YAML I/O

YAML入門

YAMLは、人間が読み取れるデータシリアライゼーション言語です。YAML言語仕様の全文はyaml.orgで読むことができます。YAMLの最も単純な形式は、「スカラー」、「マッピング」、および「シーケンス」です。スカラーは任意の数値または文字列です。ポンド/ハッシュ記号(#)はコメント行の先頭を示します。マッピングは、コロンで終わるキーと値のペアの集合です。例えば

# a mapping
name:      Tom
hat-size:  7

シーケンスは、各項目が先頭にダッシュ('-')が付いた項目のリストです。例えば

# a sequence
- x86
- x86_64
- PowerPC

インデントを使用することで、マッピングとシーケンスを組み合わせることができます。例えば、マッピングのシーケンスで、マッピング値の1つがそれ自体シーケンスであるもの

# a sequence of mappings with one key's value being a sequence
- name:      Tom
  cpus:
   - x86
   - x86_64
- name:      Bob
  cpus:
   - x86
- name:      Dan
  cpus:
   - PowerPC
   - x86

シーケンスが短いことが分かっている場合、1行あたりのエントリが多すぎて冗長になるため、YAMLは「フローシーケンス」と呼ばれるシーケンスの代替構文を提供しています。この構文では、コンマ区切りのシーケンス要素を角括弧で囲みます。上記の例は次のように簡略化できます。

# a sequence of mappings with one key's value being a flow sequence
- name:      Tom
  cpus:      [ x86, x86_64 ]
- name:      Bob
  cpus:      [ x86 ]
- name:      Dan
  cpus:      [ PowerPC, x86 ]

YAML I/O入門

インデントの使用により、YAMLは人間にとって読みやすく理解しやすくなりますが、プログラムでYAMLの読み書きを行うには、多くの面倒な詳細を扱う必要があります。YAML I/Oライブラリは、YAMLドキュメントの読み書きを構造化し、簡素化します。

YAML I/Oは、YAMLとしてダンプし、YAMLから再作成したい「ネイティブ」データ構造があると想定しています。最初のステップは、データ構造の例となるYAMLを書いてみることです。可能なYAML表現を調べてみると、データ構造からYAMLへの直接的なマッピングが必ずしも読みやすいとは限らないことが分かります。多くの場合、フィールドは人間にとって読みやすい順序になっていないか、同じ情報が複数の場所に複製されているため、人間が正しくそのようなYAMLを作成することが困難になります。

リレーショナルデータベース理論では、フィールドとテーブルを再編成する正規化と呼ばれる設計ステップがあります。同じ考慮事項が、YAMLエンコーディングの設計にも必要です。しかし、既存のネイティブデータ構造を変更したくない場合があります。そのため、YAMLを出力する際には正規化ステップが必要になる場合があり、YAMLを読み取る際には対応する非正規化ステップが必要になります。

YAML I/Oは、非侵襲的なトレイトベースの設計を使用しています。YAML I/Oはいくつかの抽象的な基本テンプレートを定義します。これらのテンプレートをデータ型で特殊化します。例えば、列挙型FooBarがある場合、その型でScalarEnumerationTraitsを特殊化し、enumeration()メソッドを定義できます。

using llvm::yaml::ScalarEnumerationTraits;
using llvm::yaml::IO;

template <>
struct ScalarEnumerationTraits<FooBar> {
  static void enumeration(IO &io, FooBar &value) {
  ...
  }
};

すべてのYAML I/Oテンプレートの特殊化と同様に、ScalarEnumerationTraitsはYAMLの読み書きの両方で使用されます。つまり、メモリ内の列挙値とYAML文字列表現間のマッピングは1か所だけです。これにより、YAMLの書き込みと解析のコードが同期した状態が保証されます。

YAMLマッピングを指定するには、llvm::yaml::MappingTraitsで特殊化を定義します。ネイティブデータ構造が既に正規化されている構造体である場合、特殊化は簡単です。例えば

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

template <>
struct MappingTraits<Person> {
  static void mapping(IO &io, Person &info) {
    io.mapRequired("name",         info.name);
    io.mapOptional("hat-size",     info.hatSize);
  }
};

データ型にbegin()/end()イテレータとpush_back()メソッドがある場合、YAMLシーケンスは自動的に推測されます。したがって、STLコンテナ(std::vector<>など)はすべて自動的にYAMLシーケンスに変換されます。

データ型の特殊化を定義したら、YAML I/Oをプログラムで使用してYAMLドキュメントを書き込むことができます。

using llvm::yaml::Output;

Person tom;
tom.name = "Tom";
tom.hatSize = 8;
Person dan;
dan.name = "Dan";
dan.hatSize = 7;
std::vector<Person> persons;
persons.push_back(tom);
persons.push_back(dan);

Output yout(llvm::outs());
yout << persons;

これは次のものを書き込みます。

- name:      Tom
  hat-size:  8
- name:      Dan
  hat-size:  7

また、次のコードを使用して、そのようなYAMLドキュメントを読み込むこともできます。

using llvm::yaml::Input;

typedef std::vector<Person> PersonList;
std::vector<PersonList> docs;

Input yin(document.getBuffer());
yin >> docs;

if ( yin.error() )
  return;

// Process read document
for ( PersonList &pl : docs ) {
  for ( Person &person : pl ) {
    cout << "name=" << person.name;
  }
}

YAMLのもう1つの機能は、単一のファイルに複数のドキュメントを定義できることです。そのため、YAMLの読み込みによってドキュメント型のベクトルが生成されます。

エラー処理

YAMLドキュメントを解析する場合、入力がスキーマ(XxxTraits<>特殊化で表現されている)と一致しない場合、YAML I/Oはエラーメッセージを出力し、Inputオブジェクトのerror()メソッドはtrueを返します。例えば、次のドキュメント

- name:      Tom
  shoe-size: 12
- name:      Dan
  hat-size:  7

には、スキーマに定義されていないキー(shoe-size)があります。YAML I/Oは自動的にこのエラーを生成します。

YAML:2:2: error: unknown key 'shoe-size'
  shoe-size:       12
  ^~~~~~~~~

スキーマに準拠していない他の入力についても、同様のエラーが発生します。

スカラー

YAMLスカラーは単なる文字列です(つまり、シーケンスでもマッピングでもありません)。YAML I/Oライブラリは、YAMLスカラーと特定のC++型間の変換をサポートしています。

組み込み型

次の型は、YAML I/Oで組み込みサポートされています。

  • bool

  • float

  • double

  • StringRef

  • std::string

  • int64_t

  • int32_t

  • int16_t

  • int8_t

  • uint64_t

  • uint32_t

  • uint16_t

  • uint8_t

つまり、MappingTraitsのフィールドまたはシーケンスの要素型としてこれらの型を使用できます。読み込み時に、YAML I/Oは検出された文字列がその型に変換可能であることを検証し、変換できない場合はエラーを返します。

ユニーク型

YAML I/Oはトレイトベースであるため、データをYAMLに変換する方法の選択はデータの型に基づいています。しかし、C++の型マッチングでは、typedefはユニークな型名を生成しません。つまり、unsigned intのtypedefが2つある場合、YAML I/Oでは両方の型はunsigned intとまったく同じように見えます。ユニークな型名を生成するために、YAML I/Oはマクロを提供しています。これは組み込み型でtypedefのように使用されますが、基本型との変換演算子を持つクラスを作成するように展開されます。例えば

LLVM_YAML_STRONG_TYPEDEF(uint32_t, MyFooFlags)
LLVM_YAML_STRONG_TYPEDEF(uint32_t, MyBarFlags)

これにより、MyFooFlagsとMyBarFlagsという2つのクラスが生成され、uint32_tの代わりにネイティブデータ構造で使用できます。これらは暗黙的にuint32_tとの間で変換されます。これらのユニークな型を作成する目的は、異なるYAML変換を取得するために、それらに対してトレイトを指定できることです。

16進型

ユニーク型の使用例として、YAML I/Oは、組み込み整数型で使用される10進形式ではなく、16進数としてYAML I/Oで書き込まれる固定サイズの符号なし整数を提供します。

  • Hex64

  • Hex32

  • Hex16

  • Hex8

uint32_tの代わりにllvm::yaml::Hex32を使用でき、YAML I/Oがその型を出力するときの違いは、16進形式でフォーマットされることです。

ScalarEnumerationTraits

YAML I/Oは、メモリ内の列挙とYAMLドキュメント内の文字列値の集合間の変換をサポートしています。これは、列挙型でScalarEnumerationTraits<>を特殊化し、enumeration()メソッドを定義することで行われます。例えば、CPUの列挙とそれをフィールドとする構造体があるとします。

enum CPUs {
  cpu_x86_64  = 5,
  cpu_x86     = 7,
  cpu_PowerPC = 8
};

struct Info {
  CPUs      cpu;
  uint32_t  flags;
};

この列挙の読み書きをサポートするために、CPUでScalarEnumerationTraitsの特殊化を定義できます。これはフィールド型として使用できます。

using llvm::yaml::ScalarEnumerationTraits;
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

template <>
struct ScalarEnumerationTraits<CPUs> {
  static void enumeration(IO &io, CPUs &value) {
    io.enumCase(value, "x86_64",  cpu_x86_64);
    io.enumCase(value, "x86",     cpu_x86);
    io.enumCase(value, "PowerPC", cpu_PowerPC);
  }
};

template <>
struct MappingTraits<Info> {
  static void mapping(IO &io, Info &info) {
    io.mapRequired("cpu",       info.cpu);
    io.mapOptional("flags",     info.flags, 0);
  }
};

YAMLを読み込むとき、検出された文字列がenumCase()メソッドで指定された文字列のいずれとも一致しない場合、エラーが自動的に生成されます。YAMLを書き込むとき、書き込まれる値がenumCase()メソッドで指定された値のいずれとも一致しない場合、ランタイムアサーションがトリガーされます。

BitValue

C++のもう1つの一般的なデータ構造は、各ビットが独自の値を持つフィールドです。これは多くの場合、「フラグ」フィールドで使用されます。YAML I/Oは、そのようなフィールドをフローシーケンスに変換するサポートをしています。例えば、次のようなビットフラグが定義されているとします。

enum {
  flagsPointy = 1
  flagsHollow = 2
  flagsFlat   = 4
  flagsRound  = 8
};

LLVM_YAML_STRONG_TYPEDEF(uint32_t, MyFlags)

MyFlagsの読み書きをサポートするために、MyFlagsでScalarBitSetTraits<>を特殊化し、ビット値とその名前を提供します。

using llvm::yaml::ScalarBitSetTraits;
using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

template <>
struct ScalarBitSetTraits<MyFlags> {
  static void bitset(IO &io, MyFlags &value) {
    io.bitSetCase(value, "hollow",  flagHollow);
    io.bitSetCase(value, "flat",    flagFlat);
    io.bitSetCase(value, "round",   flagRound);
    io.bitSetCase(value, "pointy",  flagPointy);
  }
};

struct Info {
  StringRef   name;
  MyFlags     flags;
};

template <>
struct MappingTraits<Info> {
  static void mapping(IO &io, Info& info) {
    io.mapRequired("name",  info.name);
    io.mapRequired("flags", info.flags);
   }
};

上記を使用すると、YAML I/O(書き込み時)はビットセットトレイトの各値をフラグフィールドに対してマスクし、一致するものは対応する文字列がフローシーケンスに追加されます。読み込み時には逆が行われ、不明な文字列値はエラーになります。上記のスキーマを使用すると、有効なYAMLドキュメントは次のようになります。

name:    Tom
flags:   [ pointy, flat ]

場合によっては、「フラグ」フィールドにビットマスクで定義された列挙が含まれていることがあります。

enum {
  flagsFeatureA = 1,
  flagsFeatureB = 2,
  flagsFeatureC = 4,

  flagsCPUMask = 24,

  flagsCPU1 = 8,
  flagsCPU2 = 16
};

このようなフィールドの読み書きをサポートするには、maskedBitSet()メソッドを使用し、ビット値、その名前、および列挙マスクを提供する必要があります。

template <>
struct ScalarBitSetTraits<MyFlags> {
  static void bitset(IO &io, MyFlags &value) {
    io.bitSetCase(value, "featureA",  flagsFeatureA);
    io.bitSetCase(value, "featureB",  flagsFeatureB);
    io.bitSetCase(value, "featureC",  flagsFeatureC);
    io.maskedBitSetCase(value, "CPU1",  flagsCPU1, flagsCPUMask);
    io.maskedBitSetCase(value, "CPU2",  flagsCPU2, flagsCPUMask);
  }
};

YAML I/O(書き込み時)は、列挙マスクをフラグフィールドに適用し、結果とビットセットの値を比較します。通常のビットセットの場合と同様に、一致するものは対応する文字列がフローシーケンスに追加されます。

カスタムスカラー

読みやすさのために、スカラーをカスタムの方法でフォーマットする必要がある場合があります。例えば、内部データ構造では時間を整数(あるエポックからの秒数)で使用しますが、YAMLでは、その整数を何らかの時間形式(例:4-May-2012 10:30pm)で表現する方がはるかに優れています。YAML I/Oには、データ型でScalarTraits<>を特殊化することで、スカラー型のカスタムフォーマットと解析をサポートする方法があります。書き込み時、YAML I/Oはネイティブ型を提供し、特殊化では一時的なllvm::StringRefを作成する必要があります。読み込み時、YAML I/Oはスカラーのllvm::StringRefを提供し、特殊化ではそれをネイティブデータ型に変換する必要があります。カスタムスカラー型の概要を以下に示します。

using llvm::yaml::ScalarTraits;
using llvm::yaml::IO;

template <>
struct ScalarTraits<MyCustomType> {
  static void output(const MyCustomType &value, void*,
                     llvm::raw_ostream &out) {
    out << value;  // do custom formatting here
  }
  static StringRef input(StringRef scalar, void*, MyCustomType &value) {
    // do custom parsing here.  Return the empty string on success,
    // or an error message on failure.
    return StringRef();
  }
  // Determine if this scalar needs quotes.
  static QuotingType mustQuote(StringRef) { return QuotingType::Single; }
};

ブロックスカラー

YAMLブロックスカラーは、次のように示すリテラルブロック表記を使用してYAMLで表される文字列リテラルです。

text: |
  First line
  Second line

YAML I/Oライブラリは、データ型でBlockScalarTraits<>を特殊化することで、YAMLブロックスカラーと特定のC++型間の変換をサポートしています。このライブラリは、std::stringとllvm::StringRefのような型に対してブロックスカラーI/Oの組み込みサポートを提供していません。これは、これらは既にYAML I/Oでサポートされており、デフォルトで通常のスカラー表記を使用するためです。

BlockScalarTraitsの特殊化は、ScalarTraitsの特殊化と非常に似ています。YAML I/Oはネイティブ型を提供し、特殊化では書き込み時に一時的なllvm::StringRefを作成する必要があり、ブロックスカラーの値を持つllvm::StringRefも提供し、読み込み時に特殊化ではそれをネイティブデータ型に変換する必要があります。BlockScalarTraitsの適切な特殊化を持つカスタム型の例を以下に示します。

using llvm::yaml::BlockScalarTraits;
using llvm::yaml::IO;

struct MyStringType {
  std::string Str;
};

template <>
struct BlockScalarTraits<MyStringType> {
  static void output(const MyStringType &Value, void *Ctxt,
                     llvm::raw_ostream &OS) {
    OS << Value.Str;
  }

  static StringRef input(StringRef Scalar, void *Ctxt,
                         MyStringType &Value) {
    Value.Str = Scalar.str();
    return StringRef();
  }
};

マッピング

型TをYAMLマッピングとの間で変換するには、`llvm::yaml::MappingTraits`をTで特殊化し、「`void mapping(IO &io, T&)`」メソッドを実装する必要があります。ネイティブなデータ構造が常にクラスへのポインタを使用する場合、クラスポインタで特殊化できます。例

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

// Example of struct Foo which is used by value
template <>
struct MappingTraits<Foo> {
  static void mapping(IO &io, Foo &foo) {
    io.mapOptional("size",      foo.size);
  ...
  }
};

// Example of struct Bar which is natively always a pointer
template <>
struct MappingTraits<Bar*> {
  static void mapping(IO &io, Bar *&bar) {
    io.mapOptional("size",    bar->size);
  ...
  }
};

マッピング全体を列挙型として読み込むことを許可したい状況があります。例えば、ある設定オプションが列挙型として開始したとします。その後、より複雑になったため、現在はマッピングになっています。しかし、古い設定ファイルのサポートは必要です。その場合、`ScalarEnumerationTraits::enumeration`のように`enumInput`関数を追加します。例

struct FooBarEnum {
  int Foo;
  int Bar;
  bool operator==(const FooBarEnum &R) const {
    return Foo == R.Foo && Bar == R.Bar;
  }
};

template <> struct MappingTraits<FooBarEnum> {
  static void enumInput(IO &io, FooBarEnum &Val) {
    io.enumCase(Val, "OnlyFoo", FooBarEnum({1, 0}));
    io.enumCase(Val, "OnlyBar", FooBarEnum({0, 1}));
  }
  static void mapping(IO &io, FooBarEnum &Val) {
    io.mapOptional("Foo", Val.Foo);
    io.mapOptional("Bar", Val.Bar);
  }
};

正規化なし

`mapping()`メソッドは、必要に応じて正規化と逆正規化を担当します。ネイティブなデータ構造に正規化が必要ない単純なケースでは、`mapping`メソッドは`mapOptional()`または`mapRequired()`を使用して、構造体のフィールドをYAMLキー名にバインドします。例

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

template <>
struct MappingTraits<Person> {
  static void mapping(IO &io, Person &info) {
    io.mapRequired("name",         info.name);
    io.mapOptional("hat-size",     info.hatSize);
  }
};

正規化

[逆]正規化が必要な場合、`mapping()`メソッドは正規化された値をフィールドとしてアクセスする必要があります。これを行うために、`MappingNormalization<>`というテンプレートがあり、これを使用して正規化と逆正規化を自動的に行うことができます。このテンプレートは、正規化されたキーを含む`mapping()`メソッド内のローカル変数の作成に使用されます。

極座標(距離、角度)で位置を指定する`Polar`というネイティブデータ型があるとします。

struct Polar {
  float distance;
  float angle;
};

しかし、YAMLの正規化された形式をx、y座標にすることにしました。つまり、YAMLは次のようになります。

x:   10.3
y:   -4.7

YAMLの書き込み時に極座標をx、y座標に正規化し、YAMLの読み込み時にx、y座標を極座標に逆正規化する`MappingTraits`を定義することで、これをサポートできます。

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

template <>
struct MappingTraits<Polar> {

  class NormalizedPolar {
  public:
    NormalizedPolar(IO &io)
      : x(0.0), y(0.0) {
    }
    NormalizedPolar(IO &, Polar &polar)
      : x(polar.distance * cos(polar.angle)),
        y(polar.distance * sin(polar.angle)) {
    }
    Polar denormalize(IO &) {
      return Polar(sqrt(x*x+y*y), arctan(x,y));
    }

    float        x;
    float        y;
  };

  static void mapping(IO &io, Polar &polar) {
    MappingNormalization<NormalizedPolar, Polar> keys(io, polar);

    io.mapRequired("x",    keys->x);
    io.mapRequired("y",    keys->y);
  }
};

YAMLを書き込む場合、ローカル変数「keys」は、供給された`polar`オブジェクトから構築された`NormalizedPolar`のスタック上に割り当てられたインスタンスになり、そのxおよびyフィールドを初期化します。`mapRequired()`メソッドは、xおよびyの値をキー/値ペアとして書き出します。

YAMLを読み込む場合、ローカル変数「keys」は、空のコンストラクタによって構築された`NormalizedPolar`のスタック上に割り当てられたインスタンスになります。`mapRequired`メソッドはYAMLドキュメント内で一致するキーを見つけ、`NormalizedPolar`オブジェクト`keys`のxおよびyフィールドに入力します。`mapping()`メソッドの最後にローカル変数`keys`がスコープ外になると、逆正規化メソッドが自動的に呼び出され、読み込まれた値が極座標に変換されてから、`mapping()`の第二引数に割り当てられます。

場合によっては、正規化されたクラスがネイティブ型のサブクラスであり、逆正規化メソッドによって返される可能性がありますが、一時的な正規化されたインスタンスはスタックに割り当てられています。このような場合、代わりにユーティリティテンプレート`MappingNormalizationHeap<>`を使用できます。これは`MappingNormalization<>`とほぼ同じですが、YAMLの読み込み時に正規化されたオブジェクトをヒープに割り当てます。正規化されたオブジェクトは決して破棄されません。逆正規化メソッドは、この「this」を返すことができます。

デフォルト値

`mapping()`メソッド内では、`io.mapRequired()`への呼び出しは、YAMLドキュメントの解析時にそのキーが存在することが必須であることを意味します。そうでない場合、YAML I/Oはエラーを発行します。

一方、`io.mapOptional()`で登録されたキーは、読み込まれるYAMLドキュメントに存在しなくても構いません。では、これらのオプションキーのフィールドにはどのような値が入力されるのでしょうか?これらのオプションフィールドの入力方法は2段階あります。まず、`mapping()`メソッドの第2引数は、ネイティブクラスへの参照です。そのネイティブクラスにはデフォルトコンストラクタが必要です。デフォルトコンストラクタがオプションフィールドに対して最初に設定する値が、そのフィールドの値になります。次に、`mapOptional()`メソッドには、オプションの第3引数があります。指定された場合、YAMLドキュメントにそのキーがない場合に`mapOptional()`がそのフィールドに設定する値となります。

これらの2つの方法(デフォルトコンストラクタと`mapOptional`の第3引数)には、重要な違いが1つあります。YAML I/OがYAMLドキュメントを生成する場合、`mapOptional()`の第3引数が使用されていて、書き込まれる実際の値が(==を使用して)デフォルト値と同じである場合、そのキー/値は書き込まれません。

キーの順序

YAMLドキュメントを書き出す場合、キーは`mapRequired()`/`mapOptional()`への呼び出しが`mapping()`メソッドで行われた順序で書き出されます。これにより、YAMLドキュメントの読者が自然と感じる順序でフィールドを書き出すことができます。これは、ネイティブクラス内のフィールドの順序とは異なる場合があります。

YAMLドキュメントを読み込む場合、ドキュメント内のキーは任意の順序で存在できますが、`mapRequired()`/`mapOptional()`への呼び出しが`mapping()`メソッドで行われた順序で処理されます。これにより、興味深い機能が実現します。例えば、バインドされた最初のフィールドがcpuで、2番目のフィールドがflagsであり、flagsがcpu固有の場合、cpuに基づいてflagsのYAMLへの変換方法をプログラムで切り替えることができます。これは読み込みと書き込みの両方で機能します。例

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

struct Info {
  CPUs        cpu;
  uint32_t    flags;
};

template <>
struct MappingTraits<Info> {
  static void mapping(IO &io, Info &info) {
    io.mapRequired("cpu",       info.cpu);
    // flags must come after cpu for this to work when reading yaml
    if ( info.cpu == cpu_x86_64 )
      io.mapRequired("flags",  *(My86_64Flags*)info.flags);
    else
      io.mapRequired("flags",  *(My86Flags*)info.flags);
 }
};

タグ

YAML構文は、ノードが解析される前にその型を指定する方法としてタグをサポートしています。これにより、ノードの動的な型が可能になります。しかし、YAML I/Oモデルは静的型付けを使用するため、YAML I/Oモデルでタグを使用できる方法には制限があります。最近、マップのオプションタグの確認/設定のためのYAML I/Oのサポートを追加しました。この機能を使用すると、変換可能な限り、異なるマッピングをサポートすることも可能です。

タグを確認するには、`mapping()`メソッド内で`io.mapTag()`を使用してタグを指定できます。これにより、yamlを書き込むときにもそのタグが追加されます。

検証

YAMLマップでは、キー/値ペアはそれぞれ有効ですが、組み合わせが無効な場合があります。これは、構文エラーがないのに意味的なエラーがある場合に似ています。意味レベルのチェックをサポートするために、YAML I/Oは`MappingTraits`テンプレートの特殊化でオプションの`validate()`メソッドを許可しています。

YAMLの解析時、`validate()`メソッドはマップ内のすべてのキー/値が処理された*後*に呼び出されます。入力中の`validate()`メソッドによって返されたエラーメッセージは、構文エラーのように出力されます。YAMLの書き込み時、`validate()`メソッドはYAMLキー/値が書き込まれる*前*に呼び出されます。出力中のエラーは、無効な構造体の値を持つことはプログラミングエラーであるため、`assert()`をトリガーします。

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

struct Stuff {
  ...
};

template <>
struct MappingTraits<Stuff> {
  static void mapping(IO &io, Stuff &stuff) {
  ...
  }
  static std::string validate(IO &io, Stuff &stuff) {
    // Look at all fields in 'stuff' and if there
    // are any bad values return a string describing
    // the error.  Otherwise return an empty string.
    return std::string{};
  }
};

フローマッピング

YAMLの「フローマッピング」は、YAMLへの書き込み時にインライン表記(例:`{ x: 1, y: 0 }`)を使用するマッピングです。型をYAMLでフローマッピングを使用して書き込むように指定するには、`MappingTraits`の特殊化に「`static const bool flow = true;`」を追加する必要があります。例えば

using llvm::yaml::MappingTraits;
using llvm::yaml::IO;

struct Stuff {
  ...
};

template <>
struct MappingTraits<Stuff> {
  static void mapping(IO &io, Stuff &stuff) {
    ...
  }

  static const bool flow = true;
}

フローマッピングは、`Output`オブジェクトの設定に従って改行処理されます。

シーケンス

型TをYAMLシーケンスとの間で変換するには、`llvm::yaml::SequenceTraits`をTで特殊化し、2つのメソッドを実装する必要があります。`size_t size(IO &io, T&)`と`T::value_type& element(IO &io, T&, size_t indx)`。例

template <>
struct SequenceTraits<MySeq> {
  static size_t size(IO &io, MySeq &list) { ... }
  static MySeqEl &element(IO &io, MySeq &list, size_t index) { ... }
};

`size()`メソッドは、シーケンスに現在含まれている要素数を返します。`element()`メソッドは、シーケンスのi番目の要素への参照を返します。YAMLの解析時、`element()`メソッドは現在のサイズより1つ大きいインデックスで呼び出される場合があります。`element()`メソッドは、さらに1つの要素の領域を割り当てる(要素がC++オブジェクトの場合、デフォルトコンストラクタを使用)必要があり、その新しく割り当てられた領域への参照を返します。

フローシーケンス

YAMLの「フローシーケンス」は、YAMLへの書き込み時にインライン表記(例:`[ foo, bar ]`)を使用するシーケンスです。シーケンスタイプをYAMLでフローシーケンスとして書き込むように指定するには、`SequenceTraits`の特殊化に「`static const bool flow = true;`」を追加する必要があります。例えば

template <>
struct SequenceTraits<MyList> {
  static size_t size(IO &io, MyList &list) { ... }
  static MyListEl &element(IO &io, MyList &list, size_t index) { ... }

  // The existence of this member causes YAML I/O to use a flow sequence
  static const bool flow = true;
};

上記のように、`MyList`をネイティブデータ構造のデータ型として使用した場合、YAMLに変換すると、整数のフローシーケンスが使用されます(例:`[ 10, -3, 4 ]`)。

フローシーケンスは、`Output`オブジェクトの設定に従って改行処理されます。

ユーティリティマクロ

シーケンスの一般的なソースは`std::vector<>`であるため、YAML I/Oは`LLVM_YAML_IS_SEQUENCE_VECTOR()`と`LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR()`というマクロを提供しており、これを使用して`std::vector`型で`SequenceTraits<>`を簡単に指定できます。YAML I/Oは`std::vector<>`で`SequenceTraits`を部分的に特殊化しません。これは、すべてのベクトルをシーケンスにすることを強制するためです。マクロの使用例

std::vector<MyType1>;
std::vector<MyType2>;
LLVM_YAML_IS_SEQUENCE_VECTOR(MyType1)
LLVM_YAML_IS_FLOW_SEQUENCE_VECTOR(MyType2)

ドキュメントリスト

YAMLでは、単一のYAMLファイルに複数の「ドキュメント」を定義できます。新しいドキュメントは、左揃えの「---」トークンで始まります。すべてのドキュメントの終了は、左揃えの「...」トークンで示されます。多くのYAMLユーザーは、複数のドキュメントを必要とすることはありません。YAMLスキーマの最上位ノードは、マッピングまたはシーケンスになります。そのような場合、以下は必要ありません。しかし、複数のドキュメントが必要な場合は、ドキュメントリストの種類の特性を指定できます。この特性は、SequenceTraitsと同じメソッドを持ちますが、DocumentListTraitsという名前です。例として

template <>
struct DocumentListTraits<MyDocList> {
  static size_t size(IO &io, MyDocList &list) { ... }
  static MyDocType element(IO &io, MyDocList &list, size_t index) { ... }
};

ユーザーコンテキストデータ

llvm::yaml::Inputまたはllvm::yaml::Outputオブジェクトが作成されると、そのコンストラクタはオプションの「コンテキスト」パラメータを取ります。これは、必要となる可能性のある状態情報へのポインタです。

例えば、前の例では、フラグフィールドの変換タイプを、マッピング内の別のフィールドの値に基づいて実行時に決定する方法を示しました。しかし、内部マッピングが外部マッピングのあるフィールド値を知る必要がある場合はどうでしょうか?それが「コンテキスト」パラメータの出番です。外部マップのmapping()メソッドでコンテキストに値を設定し、内部マップのmapping()メソッドでそれらの値を取得できます。

コンテキスト値は単なるvoid*です。コンテキストを使用し、ネイティブデータ型を操作するすべての特性は、コンテキスト値が実際には何かについて合意する必要があります。それは、さまざまな特性がコンテキストに依存する情報を共有するために使用するオブジェクトまたは構造体へのポインタである可能性があります。

出力

llvm::yaml::Outputクラスは、データ型に定義された特性を使用して、メモリ内のデータ構造からYAMLドキュメントを生成するために使用されます。Outputオブジェクトをインスタンス化するには、llvm::raw_ostream、オプションのコンテキストポインタ、およびオプションの折り返し列が必要です。

class Output : public IO {
public:
  Output(llvm::raw_ostream &, void *context = NULL, int WrapColumn = 70);

Outputオブジェクトを取得したら、C++ストリーム演算子を使用して、ネイティブデータをYAMLとして書き込むことができます。覚えておくべきことの1つは、YAMLファイルには複数の「ドキュメント」を含めることができることです。YAMLとしてストリームしている最上位のデータ構造がマッピング、スカラー、またはシーケンスである場合、Outputは1つのドキュメントを生成していると想定し、「---」と末尾の「...」でマッピング出力をラップします。

WrapColumnパラメータにより、フローマッピングとシーケンスは、指定された列を超えたときに折り返されます。ラッピングを完全に抑制するには、0を渡します。

using llvm::yaml::Output;

void dumpMyMapDoc(const MyMapType &info) {
  Output yout(llvm::outs());
  yout << info;
}

上記は、次のような出力を生成する可能性があります。

---
name:      Tom
hat-size:  7
...

一方、YAMLとしてストリームしている最上位のデータ構造にDocumentListTraitsの特殊化がある場合、OutputはDocumentListの各要素を走査し、各要素の先頭に「---」を生成し、「...」で終了します。

using llvm::yaml::Output;

void dumpMyMapDoc(const MyDocListType &docList) {
  Output yout(llvm::outs());
  yout << docList;
}

上記は、次のような出力を生成する可能性があります。

---
name:      Tom
hat-size:  7
---
name:      Tom
shoe-size:  11
...

入力

llvm::yaml::Inputクラスは、YAMLドキュメントをネイティブデータ構造に解析するために使用されます。Inputオブジェクトをインスタンス化するには、YAMLファイル全体のStringRefと、オプションのコンテキストポインタが必要です。

class Input : public IO {
public:
  Input(StringRef inputContent, void *context=NULL);

Inputオブジェクトを取得したら、C++ストリーム演算子を使用してドキュメントを読み取ることができます。1つのファイルに複数のYAMLドキュメントが含まれている可能性がある場合は、ドキュメントタイプのリストでDocumentListTraitsを特殊化し、そのドキュメントリストタイプでストリームインする必要があります。そうでない場合は、ドキュメントタイプをストリームインするだけで済みます。また、Inputオブジェクトのerror()メソッドを呼び出すことで、YAMLに構文エラーがあったかどうかを確認できます。例として

// Reading a single document
using llvm::yaml::Input;

Input yin(mb.getBuffer());

// Parse the YAML file
MyDocType theDoc;
yin >> theDoc;

// Check for error
if ( yin.error() )
  return;
// Reading multiple documents in one file
using llvm::yaml::Input;

LLVM_YAML_IS_DOCUMENT_LIST_VECTOR(MyDocType)

Input yin(mb.getBuffer());

// Parse the YAML file
std::vector<MyDocType> theDocList;
yin >> theDocList;

// Check for error
if ( yin.error() )
  return;