パーサ・コンビネータを使ったパーシング

ハンド・パーシング(手書き構文解析)の限界は前から分かっていました.トークンの位置に頼ったパーシングは,文法に繰り返し,オプション,シーケンスの選択などの複雑なものが出てきたとたんに手に負えなくなります.ある時点で scala のパーサ・コンビネータを使って内容モデルをパーシングをしようと決めましたが,結構長く時間がかかってしまいました.

まずは,実際に使われているスキーマから複雑な構造を紹介しましょう:

<complexType name="SubjectType">
    <choice>
        <sequence>
            <choice>
                <element ref="saml:BaseID"/>
                <element ref="saml:NameID"/>
                <element ref="saml:EncryptedID"/>
            </choice>
            <element ref="saml:SubjectConfirmation" minOccurs="0" maxOccurs="unbounded"/>
        </sequence>
        <element ref="saml:SubjectConfirmation" maxOccurs="unbounded"/>
    </choice>
</complexType>

選択肢の中にシーケンスがあり,さらにその中に選択肢があります.さて,0.0.3 だとこれをどのように扱うでしょう.

case class SubjectType(arg1: org.scalaxb.rt.DataRecord[Any]) extends org.scalaxb.rt.DataModel {
....
}

生成された case class には rt.DataRecord[Any] しかありません.二番目の選択肢が saml:SubjectConfirmation の繰り返しである可能性を考えると,これは間違いでしょう.

object SubjectType {
  def fromXML(node: scala.xml.Node): SubjectType =
    SubjectType(SubjectTypeOption.fromXML(node.child.filter(_.isInstanceOf[scala.xml.Elem])(0))) 
}

パーシングにも問題があります.まず,上の方法では最初の子要素しか読み込んでいませんが,選択肢の両者とも複数の要素を扱う必要があります.

object SubjectTypeOption {  
  def fromXML: PartialFunction[scala.xml.NodeSeq, org.scalaxb.rt.DataRecord[Any]] = {
    case x: scala.xml.Elem if (x.label == "SubjectConfirmation" && 
        x.scope.getURI(x.prefix) == "urn:oasis:names:tc:SAML:2.0:assertion") =>
      org.scalaxb.rt.DataRecord(x.scope.getURI(x.prefix), x.label, SubjectConfirmationType.fromXML(x))
  }
}

パーシングの限界は SubjectTypeOption.fromXML の実装が一つの要素しか扱っていないことのにも起因しています.つまり,複数の要素を含んだ選択肢は完全に無視されているのです.

以下は新しいバーションにより生成された同じコードです:

case class SubjectType(arg1: rt.DataRecord[Any]*) extends rt.DataModel {
     .... 
}

object SubjectType extends rt.ElemNameParser[SubjectType] {
  val targetNamespace = "urn:oasis:names:tc:SAML:2.0:assertion"

  def parser(node: scala.xml.Node): Parser[SubjectType] =
    rep((((((rt.ElemName(targetNamespace, "BaseID")) ^^ 
      (x => rt.DataRecord(x.namespace, x.name, BaseIDAbstractType.fromXML(x.node)))) ||| 
    ((rt.ElemName(targetNamespace, "NameID")) ^^ 
      (x => rt.DataRecord(x.namespace, x.name, NameIDType.fromXML(x.node)))) ||| 
    ((rt.ElemName(targetNamespace, "EncryptedID")) ^^ 
      (x => rt.DataRecord(x.namespace, x.name, EncryptedElementType.fromXML(x.node))))) ~ 
    rep(rt.ElemName(targetNamespace, "SubjectConfirmation"))) ^^ 
      { case p1 ~ 
      p2 => rt.DataRecord(null, null, SubjectTypeSequence1(p1,
      p2.map(x => SubjectConfirmationType.fromXML(x.node)).toList)) }) ||| 
    ((rt.ElemName(targetNamespace, "SubjectConfirmation")) ^^ 
      (x => rt.DataRecord(x.namespace, x.name, SubjectConfirmationType.fromXML(x.node))))) ^^
        { case p1 => SubjectType(p1.toList: _*) }
}

case class SubjectTypeSequence1(arg1: rt.DataRecord[Any],
  SubjectConfirmation: Seq[SubjectConfirmationType]) extends rt.DataModel {
    ....
}

パーシングの肝はパーサ・コンビネータにより実装されています.XMLドキュメントの対象部分から名前空間と要素ラベルのペアを抽出して, ElemName と呼ばれる形にします.次に ElemName の並びをパーサで評価します.文法の中に出てきたシーケンス構造は SubjectTypeSequence1 という形で明示的に表現されています.

繰り返しは rep(rt.ElemName(targetNamespace, "SubjectConfirmation")) というような形で表現され,これは <element ref="saml:SubjectConfirmation" minOccurs="0" maxOccurs="unbounded"/> に対応しています.