Trac に XML-RPC 経由でチケットを投稿する

list このエントリーをはてなブックマークに追加

Trac のチケット関係の api は結構あるのですが、ひとまず

  • ticket.getActions: アクションを設定する(解決済み、担当者変更など)
  • ticket.get: 指定ID のチケットを取得する
  • ticket.create: 新しいチケットを作成する
  • ticket.update: 既存のチケットを変更する、コメントを付ける
  • ticket.delete: 指定ID のチケットを削除する

なところがあれば、大丈夫かなと。細かなところはブラウザ上で操作すればよいので、完全にエミュレートするのは後ほど機会があれば作っていきます。

以下が、TracTools クラス。チケット自体は、object[] になり、そのままでは扱いづらいので Ticket クラスを作っています。

namespace moonmile.trac
{
	public class TracTools
	{
		public _Setting Setting { get; set; }

		public class _Setting
		{
			public string UserName { get; set; }
			public string Password { get; set; }
			public string Url { get; set; }
		}
		public TracTools()
		{
			this.Setting = new _Setting();
		}

		/// <summary>
		/// プロキシを作成する
		/// </summary>
		/// <returns></returns>
		private ITrac CreateProxy()
		{
			ITrac proxy = CookComputing.XmlRpc.XmlRpcProxyGen.Create<ITrac>();
			proxy.Url = this.Setting.Url;
			proxy.UserAgent = "trac-tools";
			proxy.Credentials = new NetworkCredential(
				Setting.UserName, Setting.Password);
			return proxy;
		}
		/// <summary>
		/// api をリストアップ
		/// </summary>
		/// <returns></returns>
		public List<string> ListMethods()
		{
			//プロキシクラスのインスタンスを作成
			ITrac proxy = CreateProxy();
			string [] res = proxy.SystemListMethods();
			List<string> lst = res.ToList<string>();
			return lst;
		}
		public string MethodHelp(string name)
		{
			//プロキシクラスのインスタンスを作成
			ITrac proxy = CreateProxy();
			return proxy.SystemMethodHelp(name);
		}

		/// <summary>
		/// アクションをリストアップ
		/// </summary>
		/// <param name="id"></param>
		/// <returns></returns>
		public object[] TicketGetActions(int id)
		{
			ITrac proxy = CreateProxy();
			object[] res = proxy.TicketGetActions(id);

			foreach (object[] act in res)
			{
				Console.WriteLine("{0}:{1}:{2}", act[0], act[1], act[2]);
			}
			return res;
		}

		/// <summary>
		/// 指定IDのチケットを取得
		/// </summary>
		/// <param name="id"></param>
		/// <returns></returns>
		public Ticket TicketGet(int id)
		{
			//プロキシクラスのインスタンスを作成
			ITrac proxy = CreateProxy();
			object []res =  proxy.TicketGet(id);
			return new Ticket(res);
		}

		/// <summary>
		/// チケットを新規作成
		/// </summary>
		/// <param name="ti"></param>
		/// <returns></returns>
		public int TicketCreate(Ticket ti)
		{
			ITrac proxy = CreateProxy();
			int id = proxy.TicketCreate(ti.Summary, ti.Description, ti.Attributes());
			return id;
		}

		/// <summary>
		/// チケットにコメントを追加
		/// </summary>
		/// <param name="ti"></param>
		/// <returns></returns>
		public Ticket TicketUpdate(Ticket ti)
		{
			ITrac proxy = CreateProxy();
			object[] res = proxy.TicketUpdate(ti.ID, ti.Comment, ti.Attributes());
			return new Ticket(res);
		}
		/// <summary>
		/// チケットのアクションを更新
		/// </summary>
		/// <param name="ti"></param>
		/// <returns></returns>
		public Ticket TicketUpdate(Ticket ti, string act)
		{
			ITrac proxy = CreateProxy();
			XmlRpcStruct st = ti.Attributes();
			st.Add("action", act);
			object[] res = proxy.TicketUpdate(ti.ID, ti.Comment, st);
			return new Ticket(res);
		}
		/// <summary>
		/// チケットを削除
		/// </summary>
		/// <param name="ti"></param>
		/// <returns></returns>
		public int TicketDelete(int id)
		{
			ITrac proxy = CreateProxy();
			int res = proxy.TicketDelete(id);
			return res;
		}
	}

	public interface ITrac : IXmlRpcProxy
	{
		[XmlRpcMethod("ticket.get")]
		object[] TicketGet(int id);
		[XmlRpcMethod("ticket.create")]
		int TicketCreate(string summary, string desc, XmlRpcStruct attrs);
		[XmlRpcMethod("ticket.update")]
		object[] TicketUpdate(int id,  string comment, XmlRpcStruct attrs);
		[XmlRpcMethod("ticket.delete")]
		int TicketDelete(int id);
		[XmlRpcMethod("ticket.getActions")]
		object[] TicketGetActions(int id);
	}

	/// <summary>
	/// チケットの大まかなクラス
	/// </summary>
	public class Ticket
	{
		public int ID;
		public DateTime CreateDateTime;
		public DateTime ChangeDateTime;
		public string Summary = "";
		public string Reporter = "";
		public string Owner = "";
		public string Description = "";
		public string TicketType = "";
		public string Priority = "";
		public string Comment = "";
		public object[] result = null;

		public Ticket()
		{
		}
		public Ticket(object[] ary)
		{
			this.FromArray(ary);
		}

		public Ticket FromArray(object[] ary)
		{
			Ticket ti = this;
			ti.ID = (int)ary[0];
			ti.result = ary;
			foreach (object o in ary)
			{
				XmlRpcStruct st = o as XmlRpcStruct;
				if (st != null)
				{
					foreach (string key in st.Keys)
					{
						object obj = st[key];
						Console.WriteLine("{0}:{1}:{2}", key, obj.GetType(), obj);
						switch (key)
						{
							case "owner": ti.Owner = (string)obj; break;
							case "reporter": ti.Reporter = (string)obj; break;
							case "summary": ti.Summary = (string)obj; break;
							case "description": ti.Description = (string)obj; break;
							case "time": ti.CreateDateTime = (DateTime)obj; break;
							case "changetime": ti.ChangeDateTime = (DateTime)obj; break;
							case "type": ti.TicketType = (string)obj; break;
							case "priority": ti.Priority = (string)obj; break;
						}
					}
				}
			}
			return ti;
		}
		public XmlRpcStruct Attributes()
		{
			XmlRpcStruct st = new XmlRpcStruct();
			st.Add( "owner", this.Owner );
			st.Add("reporter", this.Reporter);
			st.Add("summary", this.Summary);
			st.Add("description", this.Description);
			st.Add("type", this.TicketType);
			st.Add("priority", this.Priority);
			return st;
		}
	}
}

これを使ったコマンドラインが以下のコード。ユーザー名やらパスワードが直書きですが、これは後で、wordpress の時と同じように config ファイルから取得する予定。

namespace TracPost
{
	class Program
	{
		static void Main(string[] args)
		{
			TracTools trac = new TracTools();

			trac.Setting.Url = "http://localhost:8000/trac/gokui-ios5/login/rpc";
			trac.Setting.UserName = "masuda";
			trac.Setting.Password = "masuda";

			if (args.Length == 0)
			{
				Usage();
				return;
			}
			else
			{
				switch (args[0])
				{
					case "apilist":
						List<string> lst = trac.ListMethods();
						foreach (string name in lst)
						{
							Console.WriteLine(name);
						}
						break;
					case "help":
						if (args.Length == 2)
						{
							string txt = trac.MethodHelp(args[1]);
							Console.WriteLine(txt);
						}
						break;
					case "actions":
						if ( args.Length == 2 ) {
							trac.TicketGetActions(int.Parse(args[1]));
						}
						break;
					case "get":
						if (args.Length == 2)
						{
							Ticket ti = trac.TicketGet(int.Parse(args[1]));
							Console.WriteLine(ti.Summary);
							Console.WriteLine(ti.Description);
						}
						break;
					case "post":
						if (args.Length == 3)
						{
							Ticket ti = new Ticket();
							ti.Summary = args[1];     //  "投稿" + DateTime.Now.ToString();
							ti.Description = args[2]; //  "投稿テスト\nat" + DateTime.Now.ToString();
							ti.Reporter = "masuda";
							ti.Owner = "someone";
							int id = trac.TicketCreate(ti);
							Console.WriteLine("ID:{0}", id );
						}
						break;
					case "update":
						if (args.Length == 3)
						{
							int id = int.Parse(args[1]);
							Ticket ti = trac.TicketGet(id);
							ti.Comment = args[2];
							ti = trac.TicketUpdate(ti);
						}
						break;
					case "action":
						if (args.Length == 3)
						{
							int id = int.Parse(args[1]);
							string act = args[2];
							Ticket ti = trac.TicketGet(id);
							ti = trac.TicketUpdate(ti, act);
						}
						break;
					case "delete":
						if (args.Length == 2)
						{
							int id = int.Parse(args[1]);
							int res = trac.TicketDelete(id);
							Console.WriteLine("result:{0}", res);
						}
						break;

				}
			}
		}
		public static void Usage()
		{
			Console.WriteLine("tracpost list : api list を表示");
			Console.WriteLine("tracpost help [command]: コマンドのヘルプを表示");
			Console.WriteLine("tracpost get [id]: 指定IDのチケットを取得");
			Console.WriteLine("tracpost post [summary] [description]: チケットを登録");
			Console.WriteLine("tracpost delete [id]: 指定IDのチケットを削除");
			Console.WriteLine("tracpost update [id] [comment]: コメントを追加");
			Console.WriteLine("tracpost action [id] [resolve]: アクションを変更");
		}
	}
}

さて、このコーディングの仕方ですが、順序として「クラス」→「main 関数」の順で作っているわけではなく、「main 関数」で呼び出し側を作った後に、TicketTools クラスをそれに合わせています。
オブジェクト指向設計を思って身構えてしまうと、ついつい複雑なメソッドや余分なプロパティを作ってしまいがちですが、main 関数で、どのようにクラスを利用したいかを書いてから、クラスを実装すると、これらの無駄が省けます。ある意味で、テスト駆動に近いのですが、テスト自体にはあまり重きを置いていなくて、クラスのインターフェース(User Interface Develop Driven とか)を先に決めてどのように使うのかを主導させています。

例えば、新しいチケットをポストする場合は、

Ticket ti = new Ticket();
ti.Summary = args[1];     
ti.Description = args[2]; 
ti.Reporter = "masuda";
ti.Owner = "someone";
int id = trac.TicketCreate(ti);

のように Ticket オブジェクトを作った後に、適当にプロパティを設定しています。これは、以下のようにも書くことも可能です。

string summary = args[1];
string desc = args[2];
int id = trac.TicketCreate(summary, desc, "masuda", "someone");

この場合、TicketCreate メソッドの引数に各パラメータを指定するのですが、フィールドやら何やらが多くなってしまたときには破綻しそうですね。また、チケットの update を考えると、チケットの内容はTicket クラスを使うユーザーとしてなんらかの形でまとめておきたいところです。

で、

public class Ticket 
{
	public string summary;
	public string desc;
	public string repoter ;
	public string owner ;
}

のようにユーザー側でクラスを指定して、TicketCreate メソッドをプロパティを指定して呼び出してもいいのですが、これっていちいちユーザー側で指定するのは面倒な話です。Ticket クラスのプロパティ名などを変えたいときも困るし。

という訳で、ユーザーの気持ちになって、ユーザー側のほうからコーディングをすると、先の「trac.TicketCreate(ti)」で呼び出したほうが楽であろう、というのが、このコーディングスタイルのスタートだったりします。

カテゴリー: C#, Trac パーマリンク