まっとうなハイパースキーマがないことはシリアスな問題なんだよなー。わしゃ困るわ、フントに。ハイパースキーマに向けて; まずはセレクター。
JSONデータが与えられたとき、その部分構造の集合を取り出す(セレクトする)式があると便利です(つうか、必要なんだけど、今)。CSSのセレクターやXPathのパターンのようなものです。もちろん、JSONPathをはじめとして既にいくつもの仕様&実装が存在します。しかし、どうもオーバースペックです。もっと単純なJSON向けのセレクターを定義することにします*1。
内容:
- 構文の基本
- JavaScriptを使った説明
- いくつかの実例
- フィルタリング条件の指定
- プロパテイ名のクォーティング
- パス式としての利用
- XJSONへの拡張
- その他の拡張や略記
- BNFによる構文定義
- 課題
構文の基本
これから定義するセレクターは、ドットで区切ったフィールドアクセス式にワイルドカードを加えただけのモノです。基本的な特殊文字は次の4つです。
文字 | 説明 |
---|---|
$ | 与えられたデータ全体を表す |
. | セレクターの各ステップを区切る区切り記号 |
* | 任意のオブジェクト・プロパティを表すワイルドカード |
# | 任意の配列項目を表すワイルドカード |
基本セレクターは4種類です。
構文 | 説明 |
---|---|
'.' 名前 | 名前により指定されるプロパティ値をセレクトする |
'.' 番号 | 番号により指定される項目値をセレクトする |
'.*' | すべてのプロパティ値をセレクトする |
'.#' | すべての項目値をセレクトする |
以下はセレクター式の例です。
- $.a.y.1
- $.b.0.x
- $.a.y.#
- $.*.0
- $.b.#.x
セレクターへの入力も出力も、単一のJSONデータではなくて、データの集合です。同じデータが複数回出現してもいいので、正確にはマルチセット(バッグ)となります*2。まず、最初に対象となるデータが与えられますが、これは要素が1つのマルチセットと解釈されます。
JavaScriptを使った説明
JavaScriptを擬似言語代わりに使ってセレクターの動作を説明します。与えられたデータは次のようにセットアップされるとします。
function S(data) { this.root = data; this.currSet = [data]; }
currSetに現在セレクトされている集合(マルチセット)が入ります。初期状態では、与えらたデータだけが含まれる集合です。最初に与えられたデータはrootとして参照できます。
ルート($)と基本セレクタに対応するメソッドを次のようにします。
構文 | メソッド |
---|---|
$ | getRoot() |
'.' 名前 | getProp("名前") |
'.' 番号 | getItem(番号) |
'.*' | getAllProps() |
'.#' | getAllItems() |
先に例に挙げたセレクター式は、次のメソッドチェーンに翻訳されます。
- $.a.y.1 ⇒ getRoot().getProp("a").getProp("y").getItem(1)
- $.b.0.x ⇒ getRoot().getProp("b").getItem(0).getProp("x")
- $.a.y.# ⇒ getRoot().getProp("a").getProp("y").getAllItems()
- $.*.0 ⇒ getRoot().getAllProps().getItem(0)
- $.b.#.x ⇒ getRoot().getProp("b").getAllItems().getProp("x")
各メソッドは次のようになります。(参照とコピーにあまり注意を払ってません。元データとセレクタされたデータのそれぞれが、部分構造の共有をするかしないかは別途考える必要があります。)
S.prototype.getRoot = function() { this.currSet = [this.root]; return this; }; S.prototype.getProp = function(name) { var curr = this.currSet; var newcurr = []; for (var i = 0; i < curr.length; i++) { var item = curr[i]; if (item instanceof Object && !(item instanceof Array) && name in item) { newcurr.push(item[name]); } } this.currSet = newcurr; return this; }; S.prototype.getItem = function(index) { var curr = this.currSet; var newcurr = []; for (var i = 0; i < curr.length; i++) { var item = curr[i]; if (item instanceof Array && index < item.length) { newcurr.push(item[index]); } } this.currSet = newcurr; return this; }; S.prototype.getAllProps = function() { var curr = this.currSet; var newcurr = []; for (var i = 0; i < curr.length; i++) { var item = curr[i]; if (item instanceof Object && !(item instanceof Array)) { for(p in item) { newcurr.push(item[p]); } } } this.currSet = newcurr; return this; }; S.prototype.getAllItems = function() { var curr = this.currSet; var newcurr = []; for (var i = 0; i < curr.length; i++) { var item = curr[i]; if (item instanceof Array) { for (var j = 0; j < item.length; j++) { newcurr.push(item[j]); } } } this.currSet = newcurr; return this; };
いくつかの実例
実験用に、現在セレクトされている集合currSetを出力するメソッドを追加しておきます。
S.prototype.curr = function() { return this.currSet; };
実験用のデータは次のようにします。
var d = { "a" : { "x" : "hello", "y" : ["foo", "bar", "baz"] }, "b" : [{"x":0}, {"x":1, "y":2}], "c" : {"u": [3, 2, 1, 0]} }; var x = new S(d);
Firebugのコンソールでやってみます。
>>> x.getRoot().getProp("a").getProp("y").getItem(1).curr() // $.a.y.1 ["bar"] >>> x.getRoot().getProp("b").getItem(0).getProp("x").curr() // $.b.0.x [0] >>> x.getRoot().getProp("a").getProp("y").getAllItems().curr() // $.a.y.# ["foo", "bar", "baz"] >>> x.getRoot().getAllProps().getItem(0).curr() // $.*.0 [Object { x=0}] >>> x.getRoot().getProp("b").getAllItems().getProp("x").curr() // $.b.#.x [0, 1]
ワイルドカードが複数回出現してもかまいません。
- $.*.*
- $.*.*.#
- $.b.#.*
- $.*.#.x
>>> x.getRoot().getAllProps().getAllProps().curr() // $.*.* ["hello", ["foo", "bar", "baz"], [3, 2, 1, 0]] >>> x.getRoot().getAllProps().getAllProps().getAllItems().curr() // $.*.*.# ["foo", "bar", "baz", 3, 2, 1, 0] >>> x.getRoot().getProp("b").getAllItems().getAllProps().curr() // $.b.#.* [0, 1, 2] >>> x.getRoot().getAllProps().getAllItems().getProp("x").curr() // $.*.#.x [0, 1]
フィルタリング条件の指定
JSONPathでは、[?(条件式)] という構文で現在セレクトされている集合をフィルタリングできます。フィルタリングは必要ですが、ワイルドカードでセレクトするときに条件が付けられれば十分な気がします。そこで次のような構文を導入します。
構文 | 説明 |
---|---|
*(条件) | 条件を満たすプロパティ値だけをセレクトする |
#(条件) | 条件を満たす項目値だけをセレクトする |
この構文に対応するメソッドは次のとおり。
構文 | メソッド |
---|---|
*(条件) | getFilteredProps(条件) |
#(条件) | getFilteredItems(条件) |
メソッド引数の条件は、真偽値を返す関数として与えます。
実例を挙げましょう。
- $.b.#(x == 1)
- $.*(is_array)
- $.*(is_array).1
>>> x.getRoot().getProp("b").getFilteredItems(function(ob){return ob.x == 1;}).curr() // $.b.#(x == 1) [Object { x=1, y=2}] >>> x.getRoot().getFilteredProps(function(x){return x instanceof Array;}).curr() // $.*(is_array) [[Object { x=0}, Object { x=1, y=2}]] >>> x.getRoot().getFilteredProps(function(x){return x instanceof Array;}).getItem(1).curr() // $.*(is_array).1 [Object { x=1, y=2}]
丸括弧内の条件式を記述する構文(式言語)が必要ですが、これは別に定義してもいいので、今は触れないことにします。
プロパテイ名のクォーティング
JSONのプロパティ名は任意の文字列が使えます。特殊文字を含むプロパティ名も許されるので、クォーティングが必要です。その前に、クォートしなくてもいい名前をハッキリとさせなくてはなりません。名前の定義はものすごく難しいのですが、最近僕がよく使っているのは次の定義です。
Name ::= NameStartChar (NameChar)* NameStartChar ::= [A-Z] | "_" | [a-z] | ExtNameStartChar NameChar ::= NameStartChar | "-" | [0-9] | ExtNameChar
先頭以外ではハイフンを許しています*3。問題は、ExtNameStartCharとExtNameCharです。http://www.w3.org/TR/2008/REC-xml-20081126/#NT-Name を参考に、次のように定義すると、日本語なども使えます。
ExtNameStartChar :: = [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] ExtNameChar ::= #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
クォート文字(引用符)には、「"」以外に「'」も使えるとします。「"」と「'」は次の点以外はまったく同じです。
クォートされた文字列内でのエスケープは、JSON仕様に従います。
「'」も許すのは、セレクター式がダブルクォート文字列内に記述されることが多く、このときにエスケープしなくて済むからです。例えば、$."foo.bar".1."$t" をダブルクォート文字列内に書くと、"$.\"foo.bar\".1.\"$t\"" 、"$.'foo.bar'.1.'$t'" のほうがスッキリします。
パス式としての利用
ワイルドカード(「*」と「#」)を含まないセレクターは、ただ1つのデータからなる集合か空集合のどちらかを返します。次のような変換をすれば、セレクターをパス式として使えます。
- 結果がただ1つのデータからなる集合のときは、そのデータを返す。
- 結果が空集合のときは、エラー(例外)とするか、または未定義値を返す。
XJSONへの拡張
Catyで使っているXJSONは、タグを持ちます。タグを操作するセレクターを入れれば、XJSONに対しても使えます。
構文 | メソッド |
---|---|
'^' 名前 | getUntagged("名前") |
'^*' | getUntagged() |
'^*!' | getUntaggedExplicit() |
getUntaggedは引数のあるなしでオーバーロードしています。
これ以上詳しい話はしませんが、JSONの場合と同様に、セレクターもパスも同じ構文を使えます。
その他の拡張や略記
複数のセレクターで得られた集合を合併するユニオン演算子があると便利かもしれません。演算子記号は「|」でしょう。
ルートデータからセレクトするときは先頭のダラーは常に付けますが、省略を許したほうが便利でしょう。ダラーを省略すると、$.foo は単に foo となります。ただし、セレクターが対象とするデータが文脈により決まるときは、ダラーを省略すると意味が変わるかも知れません。
BNFによる構文定義
以下の構文は基本的なもので、XJSONへの拡張、ユニオン演算、ダラーの省略は入れてません。これらの拡張を入れると構文はもっと複雑になります。
セレクター ::= '$' 基本セレクター* 基本セレクター ::= プロパティ | 項目 | プロパティワイルドカード | 項目ワイルドカード | プロパティ条件付き | 項目条件付き プロパティ ::= '.' 名前 項目 ::= '.' 番号 プロパティワイルドカード ::= '.*' 項目ワイルドカード ::= '.#' プロパティ条件付き ::= '.*(' 条件 ')' 項目条件付き ::= '.#(' 条件 ')' 名前 ::= Name | String 番号 ::= '0' | NonZeroDigi Digit*
条件の構文は今は定義しません。NameとStringは以下のとおり。
Name ::= NameStartChar (NameChar)* NameStartChar ::= [A-Z] | "_" | [a-z] | ExtNameStartChar NameChar ::= NameStartChar | "-" | [0-9] | ExtNameChar ExtNameStartChar :: = [#xC0-#xD6] | [#xD8-#xF6] | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF] | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF] | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD] | [#x10000-#xEFFFF] ExtNameChar ::= #xB7 | [#x0300-#x036F] | [#x203F-#x2040] String ::= '"' Char* '"' | "'" Char* "'" Char ::= {特殊文字を除くUnicode文字} | Esc Esc ::= '\"' | "\'" | '\\' | '\/' | '\b' | '\f' | '\n' | '\r' | '\t' | '\u' FourHexDigits
Stringの構文定義が幾分不正確なので補足します。文字列に直接入れられない文字は:
- 引用符が「"」のときは「"」
- 引用符が「'」のときは「'」
- 番号が #x20 以下の制御文字
- 「\」