JSON のオブジェクトを ID でリンクする(4) - SakuraCrowd’s blog の続きです。
jsonId を利用しようとしていた仕様が変わってしまい、必要なくなったのでいったん保留します。
とはいっても、 {"$ref":"id1"} などの ID から、その ID を持つオブジェクトの参照への変換、逆変換の簡単なテストをクリアする実装もできたので、記念として gist にしました。長いので最後にはっときます。
jsonId の実装について
gist のコメントに書いてありますが、循環参照は jsonId の置換では問題にならないのでエラー検出はしないことにしました。
JSONPath でルートの "$" のときに jsonPath.eval の第一引数を対象にしたかったのですが、空の配列が返されてしまっていたので、その部分だけ条件文で特別に処理しました。
"$[0]" などはルート配列の1番目の要素の参照を取得できるので、 jsonPath.eval(...)[0]を使っています。
反省点
jsonId を使わない仕様をなぜ最初に気づけなかったか。
それは、なるべく作成する機能に汎用性を持たせたいという悪いくせがでたためだと思います。
tmx のプロパティの編集がやりづらいので、他の json ファイルに細かい設定を書いてそれを参照させようというのがきっかけでした。
しかし、実際に自分が必要とする設定は tmx のプロパティに CSV などで4つくらいのパラメータを書くだけですむものでした。
それがなぜか、もっと複雑なものも簡単に設定したいというあいまいな仕様になってしまいました。
実際に使うデータを処理できるだけで十分だということを念頭において仕様を考えるようにしたいです。
cocos2d-js-3.0-rc0
が使えるようになりました(∩´∀`)∩ワーイ
setup.py をいちいち管理者権限で呼び出すのが面倒なのでバッチファイルを作りました。
2.2 のころと違って html を起動するだけというわけではなく、 cocos コマンドで実行するのが微妙に面倒ですが、ノードの親子関係に依存しないで奥行きを設定できたりするのが便利です。
project.json の module に extensions を追加しないと使えないのがわからず苦戦しましたが、ScrollView で簡単にスクロールできるようになってよかったです。
(function() { | |
var root = this; | |
var sakuraCrowd = function(obj) { | |
if (obj instanceof sakuraCrowd) return obj; | |
if (!(this instanceof sakuraCrowd)) return new sakuraCrowd(obj); | |
this._wrapped = obj; | |
}; | |
if (typeof exports !== 'undefined') { | |
if (typeof module !== 'undefined' && module.exports) { | |
exports = module.exports = sakuraCrowd; | |
} | |
exports.sakuraCrowd = sakuraCrowd; | |
} else { | |
root.sakuraCrowd = sakuraCrowd; | |
} | |
// Current version. | |
sakuraCrowd.VERSION = '0.1'; | |
// Json Functions | |
// -------------------- | |
/// JSON でオブジェクトの参照を表現する拡張機能のコレクションです。 | |
/// | |
/// 参照されるオブジェクトを参照可能オブジェクト、オブジェクトの参照を表す値を参照オブジェクトと呼びます。 | |
/// 参照可能オブジェクトは ID のプロパティを持ちます。値は文字列です。既定の名前は refId です。 | |
/// 参照オブジェクトは参照のプロパティを持ちます。値は ID の文字列です。既定の名前は $ref です。 | |
/// | |
/// 用例 | |
/// jsonStr = '[{"greeting":{"$ref":"greet1"}}, {"refId":"greet1", "message":"hello"}]'; | |
/// target = JSON.parse(jsonStr); | |
/// table = sakuraCrowd.jsonId.refToObj(target); // target[0].greeting === target[1] | |
/// | |
/// 読み込んだオブジェクトのすべてのプロパティの中で参照オブジェクトをそれが指す参照可能オブジェクトに置き換えます。 | |
/// 上の例では $[0] のオブジェクトの "greeting" というプロパティの参照オブジェクトが $[1] のオブジェクトに置き換えられます。 | |
/// | |
/// sakuraCrowd.jsonId.objToRef(target); // target == [{"greeting":{"refId":"greet1", "message":"hello"}, {"$ref":"greet1"}}]; | |
/// | |
/// 同じ参照可能オブジェクトが値として出てきた場合は参照オブジェクトに置き換えます。 | |
/// 参照可能オブジェクトの定義位置は最初に出てきた位置となってしまいます。 | |
/// | |
/// sakuraCrowd.jsonId.objToRef(target, table); // target == [{"greeting":{"$ref":"greet1"}}, {"refId":"greet1", "message":"hello"}] | |
/// 位置を指定するには、refToObj の戻り値の ID テーブルを指定します。 | |
/// この例では greet1 の定義位置が $[1] であることが ID テーブルに指定されるため、 $[0].greeting ではなく $[1] で定義されます。 | |
sakuraCrowd.jsonId = {}; | |
/// ID とオブジェクトの関連を表す情報です。 | |
sakuraCrowd.jsonId.Info = function(root, table){ | |
if (typeof table === 'undefined') var table = {}; | |
/// root のオブジェクトです。これより上位のオブジェクトは表せません。 | |
this.root = root; | |
/// ID をキーにした辞書です。値ではオブジェクトを表す JSONPath を設定します。 | |
this.table = table; | |
}; | |
/// JSONPath に新しいキーを追加します。引数 arg が数値ならば [arg], 文字列ならば .arg を追加した新しい文字列を返します。 | |
/// @param {string} jsonPath JSONPath の文字列です。 | |
/// @param {string|number} key プロパティ名または要素番号です。 JSONPath に追加されます。 | |
/// @returns {string} key を追加した新しい JSONPath の文字列です。 | |
sakuraCrowd.jsonId.concatJsonKey = function(jsonPath, key) { | |
if (_.isNumber(key) == true) { | |
var jsonPath1 = jsonPath.concat('[' + key + ']'); | |
} | |
else { | |
var jsonPath1 = jsonPath.concat('.' + key); | |
} | |
return jsonPath1; | |
}; | |
/// 参照可能オブジェクトであることを判別します。 | |
/// @param {object} 判別する対象です。 | |
/// @param {string} ID プロパティの名前です。 | |
sakuraCrowd.jsonId.isReferenceableObject = function(obj, idName) { | |
if (_.isObject(obj) == false) {return false;} | |
return _.has(obj, idName); | |
}; | |
/// 参照オブジェクトであることを判別します。 | |
/// @param {object} 判別する対象です。 | |
/// @param {string} 参照プロパティの名前です。 | |
sakuraCrowd.jsonId.isReferenceObject = function(obj, refName) { | |
if (_.isObject(obj) == false) {return false;} | |
return _.has(obj, refName); | |
}; | |
/// @param {Array|object} target この中のすべての object 型の値を処理します。 | |
/// @param {string} path オブジェクトの上位の JsonPath です。 | |
/// @param {string} idName ID として識別するプロパティ名を指定します。既定では 'id' です。 | |
/// @returns {object} ID がキーで、値はオブジェクトの位置を表す JSONPath です。 | |
sakuraCrowd.jsonId._createIdObjectTable = function(target, path, idName) { | |
if (typeof idName === 'undefined') var idName = 'id'; | |
if (typeof path === 'undefined') var path = '$'; | |
var table = {}; | |
// ID を持つオブジェクトならば table に記録する。 | |
if (_.isObject(target) == true) { | |
if (_.has(target, idName) == true) { | |
var tmp = {}; | |
tmp[target[idName]] = path; | |
_.extend(table, tmp); | |
} | |
} | |
// オブジェクトと配列以外は空の辞書を返す。 | |
else if (_.isArray(target) != true) { | |
return table; | |
} | |
// オブジェクトと配列は、その要素の値について再帰的に処理する。結果は合成する。 | |
_.each(target, function(v, k, l) { | |
path1 = sakuraCrowd.jsonId.concatJsonKey(path, k); | |
_.extend(table, sakuraCrowd.jsonId._createIdObjectTable(v, path1, idName)); | |
}); | |
return table; | |
}; | |
/// @param {Array|object} target この中のすべての参照型の値を参照先のオブジェクトの値へ変換します。 | |
/// @param {string} path オブジェクトの上位の JsonPath です。 | |
/// @param {object} info sakuraCrowd.jsonId.Info です。 | |
/// @param {string} refName 参照を指定するプロパティ名を指定します。既定では '$ref' です。これを持つオブジェクトを参照型と呼びます。 | |
/// @param {array} uppers 上位のオブジェクトのリストです。循環参照を検出するために内部で使います。 | |
sakuraCrowd.jsonId._linkObjectById = function(target, path, info, refName, uppers) { | |
if (typeof refName === 'undefined') var refName = '$ref'; | |
if (typeof uppers === 'undefined') var uppers = []; | |
if (typeof path === 'undefined') var path = '$'; | |
var uppers1 = uppers.concat(); // 配列の前の要素の情報ははぶく。要素ごとの関数呼び出しで | |
uppers1.push(target); | |
// オブジェクトまたは配列の要素の値をすべて確認して、参照型ならば値を参照先のオブジェクトに変更する。 | |
if (_.isObject(target) == true || _.isArray(target) == true) { | |
_.each(target, function(v, k, l) { | |
// path の追加 | |
path1 = sakuraCrowd.jsonId.concatJsonKey(path, k); | |
// 参照型のオブジェクトの場合は、参照先のオブジェクトに変更します。 | |
if (sakuraCrowd.jsonId.isReferenceObject(v, refName) == true) { | |
if (_.has(info.table, v[refName]) == false) { | |
throw v[refName] + " は不明な ID です。"; | |
} | |
objPath = info.table[v[refName]]; | |
refObj = jsonPath.eval(info.root, objPath)[0]; | |
if (typeof refObj === "undefined") { | |
throw v[refName] + " のパス " + objPath + "は無効です。"; | |
} | |
l[k] = refObj; // l[k] の値を v から refObj に変更する。 | |
var uppers2 = uppers1.concat(); | |
uppers2.push(refObj); | |
sakuraCrowd.jsonId._linkObjectById(refObj, path1, info, refName, uppers2); | |
} | |
// 参照型以外の場合は再帰的に処理する。 | |
else { | |
sakuraCrowd.jsonId._linkObjectById(v, path1, info, refName, uppers1); | |
} | |
}); | |
} | |
return; | |
}; | |
/// ID がついているオブジェクトの情報を収集します。 | |
/// @param {object|array} target ルートのオブジェクトです。 | |
/// @param {string} offsetJSONPath オブジェクトの上位の JsonPath です。 | |
/// @param {string} idName ID として識別するプロパティ名を指定します。既定では 'id' です。 | |
/// @returns {object} sakuraCrowd.jsonId.Info です。 | |
sakuraCrowd.jsonId.createInfo = function(target, offsetJSONPath, idName) { | |
if (typeof offsetJSONPath == "undefined") var offsetJSONPath = "$"; | |
if (typeof info == "undefined") var info = new sakuraCrowd.jsonId.Info(target); | |
if (info.root !== target) throw "info.root !== target"; | |
var target1 = null; | |
if (offsetJSONPath === "$") { | |
target1 = target; | |
} else { | |
target1 = jsonPath.eval(target, offsetJSONPath)[0]; | |
} | |
// ID とオブジェクトの辞書を作成する。 | |
var table1 = sakuraCrowd.jsonId._createIdObjectTable(target1, offsetJSONPath, idName); | |
return {"root":target, "table":table1}; | |
}; | |
/// 参照オブジェクトから参照先オブジェクトへ値を変更します。 | |
/// @param {Array|object} target この中のすべての object 型の要素の値を処理します。 | |
/// @param {string} offsetJSONPath target をルートとして処理を開始する位置を JSONPath で指定します。既定では "$"です。 | |
/// @param {object} info sakuraCrowd.jsonId.Info です。処理対象ではない場所の ID を指定できます。 info.root === target でなければいけません。既定では {} です。 | |
/// @param {string} idName ID として識別するプロパティ名を指定します。既定では 'id' です。 | |
/// @param {string} refName 参照を指定するプロパティ名を指定します。既定では '$ref' です。 | |
/// @returns {object} sakuraCrowd.jsonId.Info です。変換する際に取得した情報です。 info を含みます。 | |
sakuraCrowd.jsonId.refToObj = function(target, offsetJSONPath, info, idName, refName) { | |
if (typeof offsetJSONPath == "undefined") var offsetJSONPath = "$"; | |
if (typeof info == "undefined") var info = new sakuraCrowd.jsonId.Info(target); | |
if (info.root !== target) throw "info.root !== target"; | |
var target1 = null; | |
if (offsetJSONPath === "$") { | |
target1 = target; | |
} else { | |
target1 = jsonPath.eval(target, offsetJSONPath)[0]; | |
} | |
// ID とオブジェクトの辞書を作成する。 | |
var table1 = sakuraCrowd.jsonId._createIdObjectTable(target1, offsetJSONPath, idName); | |
// 処理対象から得た ID の情報を引数の情報に追加する。 | |
_.extend(info.table, table1); | |
// info.table をもとに参照を参照先に置き換える。 | |
sakuraCrowd.jsonId._linkObjectById(target1, offsetJSONPath, info, refName); | |
return info; | |
}; | |
/// プロパティの値が参照可能オブジェクトの場合は参照オブジェクトへ変更します。 | |
/// 参照可能オブジェクトとしての定義は、最初に出現した場所か info で指定された場所です。 | |
sakuraCrowd.jsonId.objToRef = function(target, info, idName, refName, path) { | |
if (typeof info === 'undefined') var info = new sakuraCrowd.jsonId.Info(target); | |
if (typeof idName === 'undefined') var idName = 'id'; | |
if (typeof refName === 'undefined') var refName = '$ref'; | |
if (typeof path === 'undefined') var path = '$'; | |
// オブジェクトまたは配列の要素の値をすべて確認して、参照型ならば値を参照先のオブジェクトに変更する。 | |
if (_.isObject(target) == true || _.isArray(target) == true) { | |
_.each(target, function(v, k, l) { | |
// path の追加 | |
path1 = sakuraCrowd.jsonId.concatJsonKey(path, k); | |
// 参照可能オブジェクトの場合は、それを定義するパス以外の場所のものは参照オブジェクトに置き換える。 | |
if (sakuraCrowd.jsonId.isReferenceableObject(v, idName) == true) { | |
if (_.has(info.table, v[idName]) == false) { | |
info.table[v[idName]] = path1; // 空のinfoの場合などは、最初の場所を定義するパスとして追加する。 | |
} | |
var originalPath = info.table[v[idName]]; | |
if (path1 != originalPath) { | |
var tmp = {}; | |
tmp[refName] = v[idName]; | |
l[k] = tmp; | |
} | |
} | |
// 参照型以外の場合は再帰的に処理する。 | |
else { | |
sakuraCrowd.jsonId.objToRef(v, info, idName, refName, path1); | |
} | |
}); | |
} | |
return; | |
}; | |
}.call(this)); | |
describe('sakuraCrowd.jsonId.refToObj', function() { | |
it("参照オブジェクトを参照先オブジェクトに変換する", function() { | |
var target = [ | |
{"id":"id1", "value":10}, | |
{"$ref":"id1"} | |
]; | |
sakuraCrowd.jsonId.refToObj(target); | |
expect(target.length).toEqual(2); | |
expect(target[0]).toBe(target[1]); | |
}); | |
it("指定したオブジェクトの下位の場所から変換する", function() { | |
var target = [ | |
{"id":"id1", "value":10, "other":{"$ref":"id2"}}, | |
{"id":"id2", | |
"other":[ | |
{"id":"id3", "value":30}, | |
{"$ref":"id3"} | |
] | |
}, | |
]; | |
var target2 = [ | |
{"id":"id1", "value":10, "other":{"$ref":"id2"}}, | |
{"id":"id2", | |
"other":[ | |
{"id":"id3", "value":30}, | |
{"$ref":"id3"} | |
] | |
}, | |
]; | |
target2[1].other[1] = target2[1].other[0]; | |
var info = sakuraCrowd.jsonId.refToObj(target, "$[1]"); | |
expect(target).toEqual(target2); | |
expect(target[1].other[1]).toBe(target[1].other[0]); | |
}); | |
it("指定したオブジェクトの下位の場所から変換する。 info に設定した外部の参照にも対応する。", function() { | |
var target = [ | |
{"id":"id1", "value":10}, | |
{"id":"id2", | |
"other":[ | |
{"id":"id3", "value":30}, | |
{"$ref":"id1"} | |
] | |
}, | |
{"$ref":"id3"}, | |
]; | |
var target2 = [ | |
{"id":"id1", "value":10}, | |
{"id":"id2", | |
"other":[ | |
{"id":"id3", "value":30}, | |
{"$ref":"id1"} | |
] | |
}, | |
{"$ref":"id3"}, | |
]; | |
target2[1].other[1] = target[0]; | |
var info1 = sakuraCrowd.jsonId.createInfo(target, "$[0]"); | |
var info2 = sakuraCrowd.jsonId.refToObj(target, "$[1].other", info1); | |
expect(target).toEqual(target2); | |
}); | |
it("参照の ID に対応するパスがない場合はエラーとなる。", function() { | |
var target = [ | |
{"$ref":"id1"}, | |
]; | |
expect(sakuraCrowd.jsonId.refToObj.bind(null, target)).toThrow("id1" + " は不明な ID です。"); | |
}); | |
it("参照の ID に対応するパスが無効な場合はエラーとなる。", function() { | |
var target = [ | |
{"$ref":"id1"}, | |
]; | |
var info1 = {"root":target, "table":{"id1":"$.hoge"}}; | |
expect(sakuraCrowd.jsonId.refToObj.bind(null, target, "$", info1)).toThrow("id1" + " のパス " + "$.hoge" + "は無効です。"); | |
}); | |
}); | |
describe('sakuraCrowd.jsonId.objToRef', function() { | |
it("参照可能オブジェクトを参照オブジェクトに変換する(infoあり)", function() { | |
var target = [ | |
{"$ref":"id1"}, | |
{"id":"id1", "value":10} | |
]; | |
var target2 = [ | |
{"$ref":"id1"}, | |
{"id":"id1", "value":10} | |
]; | |
var expectedInfo = { | |
"root":target, | |
"table":{ | |
"id1":"$[1]" | |
} | |
}; | |
info = sakuraCrowd.jsonId.refToObj(target); | |
sakuraCrowd.jsonId.objToRef(target, info); | |
expect(target).toEqual(target2); | |
expect(info.table).toEqual(expectedInfo.table); | |
expect(info.root).toBe(expectedInfo.root); | |
}); | |
it("参照可能オブジェクトを参照オブジェクトに変換する(infoなし)", function() { | |
// 参照オブジェクトにするときに、 ID に対応した JSONPath の情報がなければ、 | |
// 最初に見つけた参照可能オブジェクトを定義として残します。それ以降は、参照オブジェクトに置換します。 | |
var target = [ | |
{"$ref":"id1"}, | |
{"id":"id1", "value":10} | |
]; | |
var target2 = [ | |
{"id":"id1", "value":10}, | |
{"$ref":"id1"} | |
]; | |
info = sakuraCrowd.jsonId.refToObj(target); | |
sakuraCrowd.jsonId.objToRef(target); | |
expect(target).toEqual(target2); | |
}); | |
}); |
<!DOCTYPE HTML> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | |
<title>Jasmine Spec Runner v2.0.0</title> | |
<link rel="shortcut icon" type="image/png" href="lib/jasmine-2.0.0/jasmine_favicon.png"> | |
<link rel="stylesheet" type="text/css" href="lib/jasmine-2.0.0/jasmine.css"> | |
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine.js"></script> | |
<script type="text/javascript" src="lib/jasmine-2.0.0/jasmine-html.js"></script> | |
<script type="text/javascript" src="lib/jasmine-2.0.0/boot.js"></script> | |
<!-- include source files here... --> | |
<script type="text/javascript" src="src/jquery-2.1.1.min.js"></script> | |
<script type="text/javascript" src="src/underscore-min.js"></script> | |
<script type="text/javascript" src="src/jsonpath.js"></script> | |
<script type="text/javascript" src="src/sakuraCrowd.js"></script> | |
<!-- include spec files here... --> | |
<script type="text/javascript" src="spec/sakuraCrowd.spec.js"></script> | |
</head> | |
<body> | |
</body> | |
</html> |
コメント
コメントを投稿