2017/02/27

JavaScriptでJSONをRailsのRansack用DSLに変換する

いろいろあってAPIサーバはRails+Ransackなのに、フロントエンドはRailsのビューテンプレートを使わずJavaScript+フレームワークのSPAを開発することがあった。

だから検索条件をAPIに渡すときに、むりやりRansackのDSLに変換しなければならなかった。これがまた不便で、複雑で、ツラすぎるので、なんとか汎用的にできないかとJavaScriptの連想配列をRansackのDSLに変換する方法を考えた。


そもそもRansackとは?


Ruby on Rails用に検索機能を簡単に実装するためのgemで、シンプルでかつ簡単に実装できる。
参考: ransack/GitHub


JSONをRansackのDSLに変換する


本来ならRailsのビューテンプレートを使えば一発なのだが、JavaScriptでフロントエンドを開発しているので、なんとかRansackのDSLに変換する。

変換元のデータ

JavaScriptであうかうデータの形式は、以下のとおり。
[
  { id: 1, name: 'rikko' },
  { age: [20, 30, 40] }
]

配列内ならOR、連想配列内ならANDでwhere (ID = 1 and NAME = 'rikko') or AGE in (20, 30, 40) みたいな条件だ。

Ransack

さきほどのデータをRansackであつかうデータの形式に変換すると、以下になる。
q: {
  g: {
    m: 'or',
    '0' => {
      id_eq: 1,
      name_eq: 'rikko'
    },
    '1' => {
      g: {
        m: 'or',
        '0' => { age_eq: 20 },
        '1' => { age_eq: 30 },
        '2' => { age_eq: 40 }
      }
    }
  }
}

age_in を使わない理由は、変換途中で連想配列にするのだが、そのときに同一キーで異なる値が設定できないから。
なので(AGE = 20 or AGE = 30 or AGE = 40)のようにINを使わない形に変換する。

RansackのDSL


HTTPリクエストで渡すためにJSON形式のDSLに変換する必要がある。その変換結果は以下のとおりになる(想定)
{
  q[g[m]]: 'or',
  q[g[0][id_eq]]: 1,
  q[g[0][name_eq]]: 'rikko'
  q[g[1][g[m]]]: 'or',
  q[g[1][g[0][age_eq]]]: 20,
  q[g[1][g[1][age_eq]]]: 30,
  q[g[1][g[2][age_eq]]]: 40,
}

もうここまでくるとどんな条件なのかひと目見ただけではわからない…。

JSONをRansackのDSLに変換する

// include: lodash 2.2.1
class Ransack {
  constructor (data) {
    this.data = data
  }
  
  /**
  * 連想配列を配列に変換
  * ex. [{ id: 1, name: 'rikko' }, { age: [20, 30, 40 ]} ]
  *   → [ [{key: 'id', val: 1}, {key: 'name', val: 'rikko'}], {key: 'age', val: [20,30,40]} ] 
  * @param  target
  * @return [[{key: *, val: *}, {key: *, val: *}], {key: *, val: *}]
  **/
  _toArray (target) {
    const result = [];
    target.forEach(a => {
      const _result = [];
      for (let key in a) {
        _result.push({ key: key, val: a[key] });
      }
      result.push(_result);
    });
    return result;
  }

  /**
  * 配列をDSLに変換
  * @param  target
  * @return DSL
  **/
  _format (target, isRecursion) {
    const result = {};
    target.forEach((a, i) => {
      if (isRecursion) {
        result['[m]'] = 'or';
      } else {
        result['g[m]'] = 'or';
      }
      _.forEach(a, (b, j) => {
        if (b.val && _.isArray(b.val)) {
          const _t = [];
          const data = b.val.map(c => {
            return { key: b.key, val: c};
          });
          _t.push(data);
          const _r = this._format(_t, true);
          _.forEach(_r, (v, k) => {
            result[`g[${i}]${k}`] = v;
          });
        } else {
          const key = isRecursion ? `[g[${j}][${b.key}_eq]]` : `g[${i}][${b.key}_eq]`;
          result[key] = b.val;
        }
      });
    });
    return result;
  }
  
  /**
  * 連想配列をRansackDSLに変換
  * ※検索条件の配列指定は1つのみ(順序は関係ない) this._formatの実装問題
  * ex. [{key: id, val: 10}] → {q[g[m]]: 'or', q[g[0][id_eq]]: 10}
  * @param _toArray
  * @return {q[g[m]]: 'or', q[g[0][*_eq]]: *}
  **/
  convertToDSL () {
    if (_.size(this.data) === 0) { return {}; }
    const ary = this._toArray(this.data);
    const formatted = this._format(ary);
    const result = {};
    for (let a in formatted) {
      result[`q[${a}]`] = formatted[a];
    }
    return result;
  }
}

const data = [
  { id: 1, name: 'rikko' },
  { age: [20, 30, 40], email: 'test@example.com' }
];
const ransack = new Ransack(data);
const result = ransack.convertToDSL();

console.log(result);



ES6の構文とlodashを使っている。
久しぶりに再帰呼び出しとか書いて、頭フットーするかと思った。

ちなみに連想配列内に2つ以上の配列が入っているとバグるので注意。
(そのバグを修正するだけの気力がなかった…。)


以上

written by @bc_rikko

0 件のコメント :

コメントを投稿