C ++またはコードジェネレーター用のJavaScript DSL-簡単です!

こんにちは、ハブロビテス!



今朝、Pythonの普遍的で強力なデータアクセスインターフェイスで突っ走ります。 わいせつな力は、あらゆる場合に多くのパラメーターの形で表され、多くの場合、非常に贅沢であり、5%の場合にのみ必要です。 その結果、単純なクエリでもパラメーターと詳細の束全体を複製する必要があり、悲観主義と何か他のことをしたいという欲求が生じます。 そして、遠い過去の似たような話を思い出しました。





問題



それはずっと前のことであり、 真実ではありませんでした 。 彼らはアウトソーシング部門を助けるために私たちを送りました。 このプロジェクトはすでにアクティブコーディングの段階にあり、何かを議論して変更するには遅すぎました(または最初は不可能だったかもしれません)。 20タブレットに保存されたデータへのビジネスレベルのインターフェイスを作成する必要がありました。 C ++で。 WindowsおよびLinuxの下。 PostgresまたはOracleで20タブレット。 「ビジネスレベルのインターフェイス」は、これらのテーブルのエンティティに対する一連の愚かな操作に対するsuch曲表現です。つまり、他のフィールドのキーまたは値で選択し、接続されたエンティティを作成、変更、または選択します。

その時までに、私たちの仕事は時々あまり面白くないかもしれないと推測し始めていました。 このタスクは、暗がりで、すべての記録を破りました。 一晩、自分の仕事に興味のない人々の靴に自分自身を感じたので、なんらかの理由でこれは私に全く刺激を与えませんでした。 この背景に対して、私の同僚はそれほど悪くはありませんでした-データベースへのアクセスを簡素化し、ステートメントに反してそれらのどれも通常Oracleと同時に動作しないことを確認したC ++(C)ライブラリの束を整理しましたとPostgres。



アイデア



これは馬鹿げた仕事であるという確信は誰によってもなされるべきであるが、私によってではなく、他の誰かのスレッドにそれを置く必要があるという考えにゆっくりと私を押し付けた。 私の環境では、そのような運命を受け入れることに同意したのはコンピューターだけでした。そのため、最終的には、必要なコードを生成することができました。 この仕事に負担をかける唯一のことは、その時点で私はこの主題を非常に複雑で自明でないものとして非常に漠然とした考えを持っていたことでした。 潜伏期から、世代のアイデアは、JSP、ビン、その他の恐怖が夜の翼で飛んでいるWebアプリケーションのデバッグのランダムな記憶によって引き出されました。 デバッガーは、JSP(Javaサーバーページ)ページから生成されたコードを示しました。 たとえば、そのようなページから:

<ul> <% for (int i = 0; i < 10; i++) { %> <li> <%= i %> <% } %> </ul>
      
      





次のコードが取得されます。

 out.println("<ul>"); for (int i = 0; i < 10; i++) { out.print("<li>"); out.print(i); out.println(); } out.println("</ul>");
      
      





つまり、HTMLを生成するJSPコードから生成するためのアルゴリズムは、おおむね非常に単純です。<%%>の内側がすべてコードになり、out.print( "<text>")の外側にラップされるすべてが、まあ、少し構文的な砂糖。 この場合、HTMLをC ++に置き換えるだけで、簡単な変換を行った後、目的のC ++コードを生成するコードを取得できます。 「他の何か」私は最小抵抗の原則に基づいて選択しました-Windowsスクリプティングホストはすでにビルドマシン上にあり、それぞれJavaスクリプト(またはECMAScript、そのまま)を使用します。



試作機



この記事では、可能な限りテキストに近いソリューションを検討します。 残念ながら、元のソリューションを考慮することはできません-あまりにも多くの重要な詳細がすでにメモリから消去されています。 居心地の良いLinuxを残さないために、Postgresデータベースのみを持っていると仮定します。Windowsスクリプティングホストは使用せず、データとしてEmployee、Departmentなどの単純な標準回路図を使用します。

JSのスタンドアロン実装として、頭に浮かんだ最初の実装であるrhino(何らかの理由でv8が2番目でした)を使用します。 そこで、rhinoを入れてcodegen.jsファイルを作成し、そこにprint(2 * 2)を書きます。 rhino codegen.js:4、出来上がり!

codegen.js
 if (arguments.length < 1) { print("Usage: codegen.js <template>"); quit(); } function produce_text(text) { return "__codegen_output += '" + text.replace("'", "\\'", 'g').replace('\n', '\\n', 'g').replace('\r', '\\r', 'g').replace('\t', '\\t', 'g') + "';\n"; } function produce_code(code) { if (code[0] == '=') { return '__codegen_output += ' + code.substr(1) + ';\n'; } return code + '\n'; } remainder = readFile(arguments[0]); var code = 'var __codegen_output = ""; '; while (remainder.length) { var begin = remainder.indexOf('<%'); if (begin >= 0) { code += produce_text(remainder.substr(0, begin)); remainder = remainder.substr(begin + 2); var end = remainder.indexOf('%>'); if (end >= 0) { code += produce_code(remainder.substr(0, end)); remainder = remainder.substr(end + 2); } else { code += produce_code(remainder); remainder = ''; } } else { code += produce_text(remainder); remainder = ''; } } code += 'print(__codegen_output);' eval(code);
      
      





-上で説明したアルゴリズムの愚かで簡単な実装-50行-コードジェネレーター全体。 テスト:

テンプレートファイル:

 <% var className = 'MyClass'; var fields = ['Name', 'Description', 'AnotherOne', 'LastOne']; %> class <%= className %> { private: <% for(var i = 0; i < fields.length; i++) { %> int <%= fields[i] %>; <% } %> };
      
      







  rhino codegen.jsテンプレート: 


 class MyClass { private: int Name; int Description; int AnotherOne; int LastOne; };
      
      





<%%>にはjsコードがあり、<%= expr%>はexprの評価結果に置き換えられます。 原則として、これが必要なものすべてであり、何でも生成するのに十分です。 残念ながら、この単純さにも欠点があることに注意する価値があります。テンプレート内のコードは非常に密集しており、適切な構文の強調表示なしで読むことは困難です。 これの最後のフィドルはJS自体によって演奏されるのではなく、簡潔さと表現力に違いはありません。

今こそ、より有用なものを生成しようとするときです。

テンプレートファイル:

 <% var model = [ { name: 'Employee', fields: { Id: { type: 'int' }, Name: { type: 'string' } } } ]; var cppTypeMap = { 'int': 'int', 'string': 'std::string' }; %> <% for (var i = 0; i < model.length; i++) { var entity = model[i];%> struct <%= entity.name %> { <% for (var field in entity.fields) { %> <%= cppTypeMap[entity.fields[field].type] %> <%= field %>; <% } %> }; <% } %>
      
      







  rhino codegen.jsテンプレート: 


 struct Employee { int Id; std::string Name; };
      
      





モデル変数の内容は、本質的にいわゆるDSL(ドメイン固有言語)です。 私たちの場合、これはサブジェクト領域のエンティティを記述するための言語です。 現在のバージョンはあまりにも原始的であり、少なくとも何らかの形で有用ではないため、必要なものをすべて追加します。

 var model = [ { name: 'Department', fields: { Id: { type: 'int' }, Name: { type: 'string'} }, primaryKey: 'Id' }, { name: 'Employee', fields: { Id: { type: 'int' }, Name: { type: 'string' }, DepartmentId: { type: 'int', references: 'Department' } }, primaryKey: 'Id' } ];
      
      







解決策



これで、キー、接続されたエンティティなどによってデータベースからエンティティを取得するためのコードを生成できます。 SQLスクリプトを生成してデータベースを作成することもすでに可能です。 1つのモデルから異なるソースを生成するために、コードを少し分散させます。
モデル
 var model = [ { name: 'Department', fields: { Id: { type: 'int' }, Name: { type: 'string'} }, primaryKey: 'Id' }, { name: 'Employee', fields: { Id: { type: 'int' }, Name: { type: 'string' }, DepartmentId: { type: 'int', references: 'Department' } }, primaryKey: 'Id' } ];
      
      





DSLのみが含まれます。
cpp.template
 <% load('model'); var cppTypeMap = { 'int': 'int', 'string': 'std::string' }; function fieldType(entity, field) { return cppTypeMap[entity.fields[field].type]; } %> <% for (var i = 0; i < model.length; i++) { var entity = model[i];%> struct <%= entity.name %> { <% for (var field in entity.fields) { %> <%= fieldType(entity, field) %> <%= field %>; <% } %> <% var fieldList = []; for (var field in entity.fields) fieldList.push(field); %> static <%= entity.name %> ByKey(<%= fieldType(entity, entity.primaryKey) %> key, pqxx::work& tr) { if (!tr.prepared("<%= entity.name %>ByKey").exists()) { tr.conn().prepare("<%= entity.name %>ByKey", "select <%= fieldList.join() %> from <%= entity.name %> where <%= entity.primaryKey %> = $1"); } pqxx::result rows = tr.prepared("<%= entity.name %>ByKey")(key).exec(query); <%= entity.name %> result; <% for (var j = 0; j < fieldList.length; j++) { %> result.<%= fieldList[j] %> = rows[0][<%= j %>].as<<%= fieldType(entity, fieldList[j]) %>>(); <% } %> return result; } <% for (var field in entity.fields) if (entity.fields[field].references) { var ref = entity.fields[field].references; %> <%= ref %> Get<%= ref %>() { return <%= ref %>::ByKey(<%= field %>); } static std::vector<<%= entity.name %>> By<%= ref %>(<%= fieldType(entity, field) %> key) { if (!tr.prepared("<%= entity.name %>By<%= ref %>").exists()) { tr.conn().prepare("<%= entity.name %>By<%= ref %>", "select <%= fieldList.join() %> from <%= entity.name %> where <%= field %> = $1"); } pqxx::result rows = tr.prepared("<%= entity.name %>By<%= ref %>")(key).exec(query); std::vector<<%= entity.name %>> result; for (pqxx::result::size_type i = 0; i < rows.size(); i++) { <%= entity.name %> row; <% for (var j = 0; j < fieldList.length; j++) { %> row.<%= fieldList[j] %> = rows[i][<%= j %>].as<<%= fieldType(entity, fieldList[j]) %>>(); <% } %> result.push_back(row); } return result; } <% } %> }; <% } %>
      
      





-正のコードを生成するためのテンプレートと
sql.template
 <% load('model'); var sqlTypeMap = { 'int': 'integer', 'string': 'text' }; function fieldType(entity, field) { return sqlTypeMap[entity.fields[field].type]; } %> <% for (var i = 0; i < model.length; i++) { var entity = model[i];%> CREATE TABLE <%= entity.name %> ( <% for (var field in entity.fields) { var ref = entity.fields[field].references; %> <%= field %> <%= fieldType(entity, field) %><% if (ref) { %> REFERENCES <%= ref %><% } %>, <% } %> PRIMARY KEY (<%= entity.primaryKey %>) ); <% } %>
      
      





-データベース作成スクリプトを生成するためのテンプレート。



  rhino codegen.js cpp.template: 


 struct Department { int Id; std::string Name; static Department ByKey(int key, pqxx::work& tr) { if (!tr.prepared("DepartmentByKey").exists()) { tr.conn().prepare("DepartmentByKey", "select Id,Name from Department where Id = $1"); } pqxx::result rows = tr.prepared("DepartmentByKey")(key).exec(query); Department result; result.Id = rows[0][0].as<int>(); result.Name = rows[0][1].as<std::string>(); return result; } }; struct Employee { int Id; std::string Name; int DepartmentId; static Employee ByKey(int key, pqxx::work& tr) { if (!tr.prepared("EmployeeByKey").exists()) { tr.conn().prepare("EmployeeByKey", "select Id,Name,DepartmentId from Employee where Id = $1"); } pqxx::result rows = tr.prepared("EmployeeByKey")(key).exec(query); Employee result; result.Id = rows[0][0].as<int>(); result.Name = rows[0][1].as<std::string>(); result.DepartmentId = rows[0][2].as<int>(); return result; } Department GetDepartment() { return Department::ByKey(DepartmentId); } static std::vector<Employee> ByDepartment(int key) { if (!tr.prepared("EmployeeByDepartment").exists()) { tr.conn().prepare("EmployeeByDepartment", "select Id,Name,DepartmentId from Employee where DepartmentId = $1"); } pqxx::result rows = tr.prepared("EmployeeByDepartment")(key).exec(query); std::vector<Employee> result; for (pqxx::result::size_type i = 0; i < rows.size(); i++) { Employee row; row.Id = rows[i][0].as<int>(); row.Name = rows[i][1].as<std::string>(); row.DepartmentId = rows[i][2].as<int>(); result.push_back(row); } return result; } };
      
      





生成されたコードは実行もコンパイルもされなかったことは認めますが、postgresで動作する実際のコードに非常に近いことを約束します。 :)



  rhino codegen.js sql.template: 


 CREATE TABLE Department ( Id integer, Name text, PRIMARY KEY (Id) ); CREATE TABLE Employee ( Id integer, Name text, DepartmentId integer REFERENCES Department, PRIMARY KEY (Id) );
      
      





多くの人にとって、新しいことを思いついていないことは明らかだと思います。 多くのORMは同じコードを生成できます。 この記事の主な目標は、DSLだけでも、独自の言語を作成することは単純ではなく、非常に単純であることを実証することでした。 多くの段階で非常にうまく保存できるので、これは見た目ほど怖くない。 たとえば、この場合、パーサーに保存しました-ほとんどの作業はJSエンジンによって行われ、コンパイラーではマシンコードよりもプラスコードを生成し、プラスコンパイラーに重みを持たせることがはるかに簡単です。



All Articles