びぼーろくっ!

誰かに見せるわけでもないけど、備忘録として。。

Node.jsでコマンド一発で全プロジェクトを最新にするツールをつくった。

ちょっと担当しているプロジェクトが増えすぎたので作成しました。
いちいちgitコマンドだったりgit Extensionでpullやcloneするのが面倒だったので・・・

やっぱツール作りは楽しいなー。

使い方。
・ProjectList.js -- 定義ファイル
・run.js -- 実行ファイル
・./shell 実行させるshellを格納

とりあえず現時点ではcloneとpullができればいいかなと思ってたので拡張はしてません。
でも拡張しやすいように作りました。

//gitCmd.sh

cd $2 && git $1;


// ProjectList.js

const projectList = [
  {
    dir: 'Hoge', // 格納したいディレクトリ
    gitRepository: 'RepositoryName', // git上のリポジトリ名
    category: 'parentDirectory', // gitRepositoryの親階層
  },
  {
    ...
  },
]
module.exports = projectList;


//run.js

const { exec } = require('child_process');

const gitUrl = 'https://exsample.com/gitbucket/git';
const projectListObjects = require('./projectList');
const shellDir = './shell';

const execute = (cmd) => {
  exec(cmd, {encoding: 'utf8'}, (err, stdout, stderr) => {
    console.log(stdout);
    if (stderr) console.error(stderr);
  });
}

const gitRun = (dir, cmd) => {
  execute(`sh ${shellDir}/gitCmd.sh "${cmd}" ${dir}`);
}

const getDir = pj => `~/Code/${pj.dir}`;

const createCloneCmd = (pj) => {
  const baseUrl = pj.category != null ? `${gitUrl}/${pj.category}` : `${gitUrl}`;
  const url = `${baseUrl}/${pj.gitRepository}.git`;

  const branch = pj.branch || 'develop';
  return `clone -b ${branch} ${url}`;
}

const clone = (pj) => {
  const dir = getDir(pj);
  const cloneCmd = createCloneCmd(pj);

  gitRun(dir, cloneCmd);
  gitRun(dir, 'pull');
}

const run = () => {
  projectListObjects.forEach((project) => {
    clone(project);
  });
}
run();

トランザクションで迷い頭の中で議論が起きた話。

こういう処理をしたいとします。
・大量のデータ&APIのリクエストを伴うため、1件ずつトランザクションを切らないと駄目。
そこで大量のデータのループ内でトランザクションを作成して、commit or rollbackすると思うのですが問題は例外発生後の処理ですね。

例:x~z~yのループ内で1件ずつTransactionを切るとして、z件目に例外が発生した場合
・z件目はrollbackして例外を握り潰してしてz~y件目を実行。
・z件目で例外発生して以後の処理は実行しない
↑2つのどちらかだと思うのだけど・・・結構ケースバイケースな事象な気がしてならないんです。

        //ケース1
        public async Task LoopTransactionCase1()
        {
            var idList = await repository.GetIdList().ConfigureAwait(false);
            foreach (var id in idList)
            {
                using (var scope = new TransactionScope(TransactionScopeOption.Required, 
                    new TransactionOptions() { IsolationLevel = IsolationLevel.ReadCommitted },
                    TransactionScopeAsyncFlowOption.Enabled))
                {
                    //POSTデータを取得して作成
                    var data = await repository.GetPostData(id).ConfigureAwait(false);
                    var content = new StringContent(JsonConvert.SerializeObject(data));
                    content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

                    //POST経由でAPIにリクエストを飛ばす。戻りはboolean
                    //httpClientが失敗した場合、例外を潰しfalseを返却する。
                    var response = await httpClient.PostAsync("url", content).ConfigureAwait(false);
                    if (!await deserializeResponse<bool>(response))
                    {
                        throw new SystemException("failed to data registration.");
                    }
                    scope.Complete();
                }
            }
        }

        //ケース2
        public async Task LoopTransactionCase2()
        {
            var idList = await repository.GetIdList().ConfigureAwait(false);
            foreach (var id in idList)
            {
                using (var scope = new TransactionScope(TransactionScopeOption.Required,
                    new TransactionOptions() { IsolationLevel = IsolationLevel.ReadCommitted },
                    TransactionScopeAsyncFlowOption.Enabled))
                {
                    var data = await repository.GetPostData(id).ConfigureAwait(false);
                    var content = new StringContent(JsonConvert.SerializeObject(data));
                    content.Headers.ContentType = new MediaTypeHeaderValue("application/json");

                    var response = await httpClient.PostAsync("url", content).ConfigureAwait(false);
                    if (!await deserializeResponse<bool>(response))
                    {
                        
                        scope.Dispose();
                        continue;
                    }
                    scope.Complete();
                }
            }
        }

問題はケース1の場合、不正データがz件目に存在していた場合以後のデータが処理されないことです。
そのため不正なデータのせいでz + 1 ~ y件までのデータ処理が正常であっても走らない事が問題です。

ケース2の問題としては仮にx~z~yが10000件を超えるデータは全て正常だけどリクエスト先の鯖が落ちてる or clientの通信が切れてる。
などの問題があった場合、無駄な処理を10000件を実行することになります。

うーん、これはどっちがいいんだろう。。。

モデル同士の値を比較する共通メソッドをつくった。

C#やっぱしんどいです。
今オブジェクトの比較事態はJSON文字列に変換して行っているのですが、Nunitの仕様上擦り切れてしまうんですよね。。
コンソール画面で色つきで表示されたら不正な値が見れるからいいのになー。と思ってて探してみたけど無かったので作りました。

結構無理やりな勢いで作ったので普通にバグるかもしれません。。使用するのは自己責任で・・・
JavaScriptなら簡単にいけちゃうのにな。。。やっぱC#しんどry
実際にモデルの比較ならJSONに変換したほうがイケてると思います。今回は細かい検証用なので使い道が少ないかも。。

    public void AssertResultConsoleView<T>(T modelA, T modelB)
    {
        var propsA = modelA.GetType().GetProperties();
        var propsB = modelB.GetType().GetProperties();

        var i = 0;
        foreach (var prop in propsA)
        {
            Console.ResetColor();
            var propB = propsB[i];
            if (prop.Name == propB.Name)
            {
                var aValue = prop.GetValue(modelA).ToString();
                var bValue = propB.GetValue(modelB).ToString();

                if (aValue != bValue)
                {
                    Console.BackgroundColor = ConsoleColor.Red;
                }
                Console.WriteLine("比較結果:{0}: exp:{1} result:{2}", prop.Name, aValue, bValue);
            }
            i++;
        }
    }

知らなかった!SELECT文の結果をINSERTする方法

今回はSQLです。(postgresqlを使用)
本番環境のマスタースクリプトを更新する作業があったのですが、本番環境のデータは参照できないので
正規表現などで一気にスクリプトファイルを組むわけにもいかずもやもやしてました。

調べた結果SELECT-INSERTなるものがあったのですね!
(DB周りは他の人が担当してたので詳しい知識はなかった orz)

こんなテーブルやレコードがあるとします。

insert_table

id colomn2 colomn3
null null null

select_table

id birthday job
1 1995/01/01 developer
2 1996/01/01 lawyer
3 1997/01/01 docter

やりたい事はinsert_tableにselect_tableのidを反映したい場合です。
固定値が混ざったレコードを登録したい場合はSELECT文に固定値を書き込めばいいです。

    • insert_table(登録したいレコードのテーブル
    • select_table(既に存在するテーブル)
INSERT INTO insert_table(
     id, column2, column3
)
SELECT id, '固定値', now()
FROM   select_table;

結果をみてみましょう・・・

SELECT * FROM insert_table;

insert_table

id colomn2 colomn3
1 固定値 2017/4/6
2 固定値 2017/4/6
3 固定値 2017/4/6

今日は以上です。

どうでもいいけど・・・はてぶろってpostgresqlの記法ってないのかな・・
mysqlでコードハイライトしたけど、それっぽいのが見つからなかった・・・

MockHttpでHttpResponseをモック化!

最近C#ばっかりです。
なかなか型の制約とかHttpContextがHttpContextBaseを継承してなかったりとか色々としんどいです。

今回戸惑ったのがAPIのRequest⇔ResponseをMock化したかったのですが
NUGET辺りで探してたらいいのがありました。

これです。
github.com


今回はNUnitFrameWorkを使用しています。

using Newtonsoft.Json;
using NUnit.Framework;
using RichardSzalay.MockHttp;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Exsample.Test.Common.Util;
using Exsample.Net.Http;
using Moq;
using System.Web;
using System;

namespace Exsample.Module.Test
{
    [TestFixture]
    public class GetStreamAsync : ClientContext
    {
        private string url;
        private Stream stream;
        private Error error;

        /// <summary>
        /// 成功時(status=OK)のMockResponseMessageをセット
        /// </summary>
        private void setSuccessContent()
        {
            mockHttpMessage.Expect($"{baseUrl}*")
                .Respond(() =>
                {
                    // Status 200時に実行されるCallBack。
                    // Task<HttpResponseMessage>型を返すようにする。
                    return Task.Factory.StartNew<HttpResponseMessage>(() =>
                    {
                        var res = new HttpResponseMessage();
                        res.Content = new StreamContent(stream);
                        return res;
                    });
                });
        }

        /// <summary>
        /// 失敗時(status=NG)のMockResponseMessageをセット
        /// </summary>
        private void setFailContent()
        {
            error = new Error();
            // シリアライズして文字列として第二引数に渡してます。
            var content = JsonConvert.SerializeObject(error);
            mockHttpMessage.Expect($"{baseUrl}*")
                .Respond(HttpStatusCode.InternalServerError, "application/json", content);
        }

        /// <summary>
        /// MOCKのセットアップ。
        /// </summary>
        /// <param name="isSuccess">成功テストか否か</param>
        public void mockSetup(bool isSuccess)
        {
            if (isSuccess)
            {
                // テスト用のダミーStreamを作成
                stream = StreamCreator.Create();
                url = "unittest/getasync";
                setSuccessContent();
            }
            else
            {
                url = "unittest/notfound";
                setFailContent();
            }
        }

        /// <summary>
        /// Status=OKのテスト
        /// </summary>
        /// <returns></returns>
        [Test]
        public async Task Success()
        {
            mockSetup(true);
            var result = await client.GetStreamAsync(url, null);
            Assert.AreEqual(stream, result);
        }

        /// <summary>
        /// Status=NGのテスト
        /// </summary>
        /// <returns></returns>
        [Test]
        public void Fail()
        {
            mockSetup(false);
            var exception = Assert.ThrowsAsync<ResponsException>(() =>
            {
                return client.GetStreamAsync(url, null);
            });
            Assert.AreEqual(HttpStatusCode.InternalServerError, exception.StatusCode);
            // クラス同時の比較は出来ないのでJSON化して比較
            AssertObject(error, JsonConvert.DeserializeObject<Error>(exception.Message));
        }
    }

    /// <summary>
    /// パラメーターのインスタンス化や共通のMock化を行う
    /// </summary>
    public class ClientContext : TestBase
    {
        protected TargetClient client; // テスト対象のメソッド
        Mock<HttpContextBase> mockContext;
        protected MockHttpMessageHandler mockHttpMessage;
        protected Guid userId;

        // 適当なURL
        protected string baseUrl = "http://exsample.com:8080/api/";

        protected class Error
        {
            public string ErrorDiscription { get; set; }
            public string ErrorMessage { get; set; }
            public Error()
            {
                this.ErrorDiscription = "error_description";
                this.ErrorMessage = "server_error";
            }
        }

        [SetUp]
        public void Setup()
        {
            // MockのHttpContextBaseを作成
            mockContext = FakeHttpContextBase.Create(); // HttpContextBaseをMock化をする。

            // Sessionを上書きする
            var session = new Mock<HttpSessionStateBase>();
            userId = Guid.NewGuid();
            session.Setup(x => x["userId"]).Returns(userId.ToString());
            mockContext.Setup(x => x.Session).Returns(session.Object);

            // 今回紹介するmockHttpMessageHandlerのインスタンスを作成。
            mockHttpMessage = new MockHttpMessageHandler();
            var httpClient = new HttpClient(mockHttpMessage);

            client = new TargetClient(mockContext.Object);
            client.HttpClient = httpClient;
        }
    }
}

テスト対象メソッドはStatus=NGならばResponsExceptionをスロー。
メッセージ部分はJSON形式にて返却されます。
成功時は返却されたHttpResponseMessage.ContentをStreamにして返却しています。

APIのクライアント部分だし通信されると困るので、何かいいメソッドがないか見てたらいいのを見つけました。
今度もこれを使ってゴリゴリ書いていきたいと思います。

gulpを使ってlessをコンパイルする方法

今Electronを使ってデスクトップアプリを趣味で開発しているのですが
猛烈にlessを導入したくなったので色々と設定してみました。

less
lesscss.org


ただ、Client変換方式でlessを導入するのはブラウザやパフォーマンスの問題があるので
lessファイルをコンパイルしてくれてコンパイル後のcssを読み取るように設定してみます。
・・もちろんlessの公式にあるとおり

 lessc styles.less styles.css

このようなコマンドでも実行は可能ですが、いちいちCSSの編集後にこのコマンドを投げるのは面倒です。
そのため保存時に自動的にコンパイルが走り、特定のフォルダ内にCSSが作成されるようにします。

上記のことをやりたい場合、gulp + gulp-lessを使用すると便利です。

gulp.js

www.npmjs.com

まずはプロジェクトにlessを導入

npm install --save-dev less

そしてgulp-less(gulp上でlessをビルドしてくれる機能)も導入します。

npm install --save-dev gulp-less

そして新規にgulpfile.jsをルートに作成します。

//gulpfile.js
const gulp = require('gulp');
const less = require('gulp-less');

// less → cssにコンパイルするタスク
gulp.task('less', function(){
  src('./src/less/*.less')
  .pipe(less())
  .pipe(gulp.dest('/src/css/*'));
});

//ファイルの変更があった場合、実行されるタスク
gulp.watch('watch', ['less'], function() {
  gulp.watch('./src/less/*.less', ['less']);
});

// デフォルトで実行してもらいたいタスクを配列で定義する。
gulp.task('default', ['less']);

すごく直感的に書けると思います。
まずはgulp.taskの第一引数に'less'という名前を指定しました。
これでgulp lessというコマンドを実行したら第二引数のcallbackが実行されます。
callbackの中身はlessファイルをcssコンパイルするという動作ですね。

そしてgulp.watchでファイルの変更があった場合(このソースの場合、.src.less/*.lessの変更)
gulp.taskで指定されたlessを実行するという定義になります。

また、第二引数の['less']ですがgulp watchを実行する前に一度走らせたいタスクを配列で記入していきます。
gulp watchを実行する前にlessを変更した場合にも実行して貰いたいので。

最後にpackage.jsonにこちらを記入します。
今回はlessのコマンド実行なのでスクリプト名をlessにしていますが任意の名前で大丈夫です。

//package.json
scripts: {
    "watch":  "gulp watch";
}

そしてpackage.jsonのある階層で下記を実行するとコンパイルされるはずです。

npm run watch

設定自体楽チンで、何よりcssよりも綺麗に書けるのでlessの導入はお勧めです。ぜひgulpをお試し下さい!

仕様変更で没になったので・・・

仕様変更で没になったので・・・

せっかく書いたのに悔しいので載せます。
ソースを見れば一目ですが、HTTPでリクエストを飛ばす処理になってます。
時間が無くて動作確認してないです。多分POSTでつまづくかも。。


簡単な解説
DELETEやPUT、PATCHは未記入ですが同じような要領でいけます。
戻りをTモデルにしてあるのでどのクラスでもマッピングしてくれるように書きました。
リクエスト先の戻りはJSONを想定して書いてます。

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading.Tasks;
using System.Collections;

namespace MyNameSpace.Request
{
    public class RequestService
    {
        /// <summary>
        /// GETメソッド
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public async Task<T> Get<T>(string url, Dictionary<string, dynamic> parameters = null)
        {
            var queryParams = createQueryParameter(parameters);
            var http = new HttpClient();
            http.DefaultRequestHeaders.Add("contentType", "application/json");

            var res = await http.GetAsync(url + queryParams);
            var body = await res.Content.ReadAsStringAsync();

            return serializeBody<T>(body);
        }

        /// <summary>
        /// POST
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        public async Task<T> Post<T>(string url, object data)
        {
            var http = new HttpClient();
            http.DefaultRequestHeaders.Add("contentType", "application/json");

            var param = createNameValueCollection(data);
            var content = new FormUrlEncodedContent(param.ToArray());
            var body = "";
            try
            {
                var res = await http.PostAsync(url, content);
                body = await res.Content.ReadAsStringAsync();
            }
            catch (Exception e)
            {
                var msg = e.Message;
            }
            return serializeBody<T>(body);
        }

        /// <summary>
        /// 受け取ったJSONをTモデルにシリアライズする
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private T serializeBody<T>(string body)
        {
            using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(body)))
            {
                var serializer = new DataContractJsonSerializer(typeof(T));
                return (T)serializer.ReadObject(stream);
            }
        }

        /// <summary>
        /// 送信用のデータをキャストする
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private List<KeyValuePair<string, string>> createNameValueCollection(object data)
        {
            var bodies = new List<KeyValuePair<string, string>>();
            var type = data.GetType();
            foreach (var prop in type.GetProperties())
            {
                var name = prop.Name;

                if (prop.PropertyType.IsArray)
                {
                    var values = (IEnumerable)prop.GetValue(data, null);
                    foreach (var arrProp in values)
                    {
                        var arrValue = arrProp.ToString();
                        bodies.Add(new KeyValuePair<string, string>(name, arrValue));
                    }
                } else if (prop.PropertyType.Name == "DateTime") {
                    var value = ((DateTime)prop.GetValue(data, null)).ToShortDateString();
                    bodies.Add(new KeyValuePair<string, string>(name, value));

                } else {
                    var value = prop.GetValue(data, null).ToString();
                    bodies.Add(new KeyValuePair<string, string>(name, value));
                }
            }
            return bodies;
        }

        /// <summary>
        /// GET専用・クエリパラメータを作成
        /// </summary>
        /// <param name="data"></param>
        /// <returns></returns>
        private string createQueryParameter(Dictionary<string, dynamic> parameters)
        {
            if (parameters == null || parameters.Count == 0) return "";
            var list = new List<string>();
            foreach(var param in parameters)
            {
                var builder = new StringBuilder();
                builder.Append(param.Key).Append("=").Append(param.Value);
                list.Add(builder.ToString());
            }
            var arr = list.ToArray();
            return "?" + string.Join("&", arr);
        }
    }
}