dynamic を利用して ExDoc を書き直してみる(1)

list このエントリーをはてなブックマークに追加

「メタプログラミング .NET」をざっと読み終わったので、手元の github から ExDoc をダウンロードしてきて、dynamic 版の ExDoc を作っているところです。

旧 ExDoc は、演算子のオーバーライトと暗黙のキャストを使って、/,*,%演算子を使って XML を探索できていました。まあ、奇妙といえば奇妙だけど、LINQ の派生みたいな感じで作れるわけです。タグを指定するのが文字列なのがあれなのと、だったら XPath で指定してもいいんじゃないか、という話もありますが、まあ実験的に。ちなみに、このライブラリは実運用で4年ほど使っています。CakePHP をサーバーにして、クライアント側の XML パースを ExDoc を使ってパースしているという技ですね。

public void TestQuery()
{
    var xml = "<root>" + 
        "<person id='1'>masuda</person>" +  
        "<person id='2'>tomoaki</person>" +  
        "<person id='3'>yumi</person>" +  
        "</root>";

    var doc = ExDocument.LoadXml(xml);

    // query element
    ExElement el = doc * "person" % "id" == "2";
    Assert.AreEqual("tomoaki", el.Value);
    // query elements
    ExElements els = doc * "person";
    Assert.AreEqual(3, els.Count);
    Assert.AreEqual("masuda", els[0].Value);
    Assert.AreEqual("tomoaki", els[1].Value);
    Assert.AreEqual("yumi", els[2].Value);
}

で、常々、PHP の SimepleXML がうらやましかった訳ですが、そんな風に直接プロパティで設定できるのが、dynamic の良いところです。まあ、インテリセンスが効かなくなってしまうのですが、どうせ XML タグ名なのだから、そこは割り切りということで。dynamic で受けて、指定の型へキャストし直して使う方法もあるので、それもおいおいと実装する予定。「メタプログラミング .NET」にも書いてあるけど、複数のインターフェースを使って、片方のインターフェースを隠す技が使えます。確か、Xamarin.Forms の XAML ツリーのところで使っています。

dynamic 版の ExDoc を使うと、こんな風に XML タグをプロパティのように扱えます。属性に連想配列を使っているのは、SimpleXML の真似でもあるのと、doc.person.@id のようにはできなかったからなのです。先頭の @ は有効になるんだけど、TryGetMember メンバでだと、「id」としか取れないので、「@id」と「id」は同じ名前として扱われるためです。残念。

public void TestQuery()
{
    var xml = @"
<root>
<person id='1'>masuda</person>
<person id='2'>tomoaki</person>
<person id='3'>yumi</person>
</root>
";
    var doc = ExDocument.LoadXml(xml);
    // query element
    var el = doc.person["id"] == "2";
    Assert.AreEqual("tomoaki", el.Value );
    // query elements
    var els = doc.person;
    Assert.AreEqual(3, els.Count);
    Assert.AreEqual("masuda", els[0].Value);
    Assert.AreEqual("tomoaki", els[1].Value);
    Assert.AreEqual("yumi", els[2].Value);
}

まあ、それでも、連想配列は属性で、配列は子要素で示せるわけで、なかなかいい感じでパースができます。

子孫要素も含めて調べるときは「*」演算子のかわりに、タグ名に「_」を付けます。プロパティ名やメソッド名を後付けでできる(実行時に内部で分解して分岐させる)のが dynamic の良いところです。

public void TestSelectAttr()
{
    var xml = @"
<html>
<body>
<a id='a1' href='link001.html'>title</a>
<a id='a2' href='link002.html'>title</a>
</body>
</html>
";

    var st = new StringReader(xml);
    var doc = XDocument.Load(st);
    var edoc = ExDocument.Load(doc);

    var el = edoc._a["id"] == "a2";
    Assert.AreEqual("title", el.Value);
    Assert.AreEqual("link002.html", el["href"]);
}

これらをどう実装しているかというと、参照だけならば以下で ok です。

– System.Dynamic.DynamicObject を継承する
– TryGetMember を実装する
– TryGetIndex を実装する
– == 演算子を再定義する。

まあ、先行きは値の設定までやるので、TrySetMember や TrySetIndex も実装していきます。
具体的な説明は、.net core の DynamicObject のコードを読むか(コードに詳しい説明が書いてあります)、「メタプログラミング .NET」にざっと書いてあります。

public class ExElement : System.Dynamic.DynamicObject
{
    public string TagName { get { return _el.Name.LocalName; } }
    public string Name {  get { return TagName; } }
    public string Value { get { return _el.Value; } }
    internal XElement _el = null;

    public ExElement( XElement el = null )
    {
        _el = el;
    }
    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        var els = new ExElements();

        if (binder.Name[0] == '_')
        {
            string name = binder.Name.Substring(1);
            els = SelectNodes(_el, els, name, true);
        } else
        {
            els = SelectNodes(_el, els, binder.Name);
        }
        result = els;
        return true;
    }
    private ExElements SelectNodes( XElement x, ExElements els, string name, bool deep = false )
    {
        foreach (var el in x.Elements())
        {
            if (name == el.Name)
            {
                els.Add(new ExElement(el));
            }
            if (deep)
                els = SelectNodes(el, els, name, true);
        }
        return els;
    }

    public override bool TryGetIndex(GetIndexBinder binder, object[] indexes, out object result)
    {
        result = ExDocument.EmptyElement;
        if (indexes.Count() == 1)
        {
            if (indexes[0] is int)
            {
                int i = (int)indexes[0];
                if (i < _el.Parent.Elements().Count())
                {
                    result = new ExElement(_el.Parent.Elements().ToList()&#91;i&#93;);
                }
            }
            else if (indexes&#91;0&#93; is string)
            {
                string s = indexes&#91;0&#93; as string;
                var attr = _el.Attribute(s);
                if (attr != null)
                {
                    result = new ExAttr(attr);
                }
            }
        }
        return true;
    }

    /// <summary>
    /// 探索
    /// </summary>
    /// <param name="lst"></param>
    /// <param name="value"></param>
    /// <returns></returns>
    public static ExElements operator ==(ExElement el, string value)
    {
        var result = new ExElements();
        foreach (var it in el._el.Elements())
        {
            if (it.Value == value)
            {
                result.Add(new ExElement(it));
            }
        }
        return result;
    }
    public static ExElements operator !=(ExElement el, string value)
    {
        var result = new ExElements();
        foreach (var it in el._el.Elements())
        {
            if (it.Value != value)
            {
                result.Add(new ExElement( it));
            }
        }
        return result;
    }
    public override bool Equals(object obj)
    {
        return base.Equals(obj);
    }
    public override int GetHashCode()
    {
        return base.GetHashCode();
    }

    /// <summary>
    /// 文字列にキャスト
    /// </summary>
    /// <param name="el"></param>
    public static implicit operator string(ExElement el)
    {
        return el.Value;
    }
    /// <summary>
    /// 数値にキャスト
    /// </summary>
    /// <param name="el"></param>
    public static implicit operator int(ExElement el)
    {
        return int.Parse(el.Value);
    }
    /// <summary>
    /// 実数にキャスト
    /// </summary>
    /// <param name="el"></param>
    public static implicit operator double(ExElement el)
    {
        return double.Parse(el.Value);
    }
}

もうちょっと整理して、前バージョンの ExDoc と同じように使えたら NuGet にアップしましょう。

カテゴリー: C#, EXDoc パーマリンク