記事 Hiroshi Sato · 4 hr 前 13m read

Claude CodeでObjectScriptプログラミング

最近話題のClaude Codeを使って、ObjectScriptプログラミングをトライしてみました。

もちろんClaude CodeにObjectScriptのコードを書かせるにはそれなりの指示が必要ですが、適切なプロンプトを与えると想像以上にちゃんとしたObjectScriptコードを書いてくれます。

今回試したのは、少し前に投稿したCSPの#server問題に対応するため、#serverをREST APIに書き換えるというものです。

元のソースは、

https://github.com/wolfman0719/shopdemo

これはCSPのデモアプリケーションですが、しっかりと#server機能を使用しています。


結論をいうと、%sessionがRESTとCSP間で共有できないという根本問題があって、実行成功までには至っていませんが

面倒なコーディングをかなりカーバーしてくれることがわかります。

そして、Claude Codeは、 ObjectScriptのことかなり理解していてくれます。

ObjectScriptプログラマーが少ない問題を大きく改善してくれる可能性があります。
 

それでは、私がどのようなことを行ったか説明します。

まず、Claude Codeに以下のような指示を与えました。

/Users/hsatoctr/claude/syncの下にインターシステムズのIRIS上で動作するCSPのウエブアプリケーションのコードがあります。
/Users/hsatoctr/claude/sync/sample/cspの下には2つのcspファイルがあります。

synccall.cspは、CSPのハイパーイベントと呼ばれる#serverコールを使用してブラウザ上のjavascriptからIRISのサーバー上のメソッドを呼び出しています。

しかし、#serverはSynchronous XMLHttpRequestという推奨されていない機能を利用しているため、他手段への書き換えが推奨されています。

restcall.cspは、synccall.cspを一般的なRESTによって書き換えを行ったコードです。

RESTコールは、axiosを使用して実装しています。

呼び出されるIRISのメソッドは、synccall.cspの中で定義されているMyMethodですが、RESTからこのメソッドは直接呼び出せませんので、

/Users/hsatoctr/claude/sync/sample/method/Samples/API.clsにUrlMapの定義とともにメソッドの内容も移動しています。
メソッド名もGetMessageに変更しています。
データの戻し方もRESTで一般的なJSON形式で返します。
IRISのObjectScriptでは、JSON形式のデータは、ダイナミックオブジェクトとして作成します。

CreateAPIDefinitionメソッドは、REST APIのIRIS上の定義を登録するためのコードを記述しています。

/Users/hsatoctr/claude/targetにmain.cspというCSPファイルがあります。

このファイルの中でも#server呼び出しを行なっている部分があるので、同じようにaxiosを使用したREST呼び出しに変更してください

sampleと同様に呼び出されるメソッドもShop.APIという名前のIRISクラス定義(ファイル名は、API.cls)として定義してください

CreateAPIDefinitionメソッドも同様に記述してください

そのアプリケーション名は、/api/shopにしてください

ディスパッチクラス名は、Shop.APIです

main.cspファイルに定義されているAddEntryメソッドの中には$JS<>という記述があります。
これはCSPの特別な機能で、IRISのメソッド内でJavascriptコードの実行ができる機能です。
これはREST APIのメソッド内でば動作しないので、javascriptで設定している値を一時的にObjectScriptのダイナミックオブジェクトに設定し、JSON形式で呼び出し元のjavascriptに返します。

例えば、

&JS<alert('ログアウトされました。\n再度ログインしてください。');top.document.location = 'login.htm';>

という記述がありますが、メソッド内では

 set return = {}
 set return.LogoutMessage = "ログアウトされました。\n再度ログインしてください。 "

のように記述し、

呼び出し元のjavascriptでは、LoginMessageが存在したら、Alertを呼び出してLoginMessageの内容を表示する処理と
top.document.location = 'login.htm';を加えてください

そしてAddEntryメソッドの中に以下のコードがありますが、

 // かごにエントリを追加
 if +amount=0 {
   // 個数の入力領域から入力された文字列を削除
   &JS<form.amount#(id)#.value = "";>
   // 個数が0の場合、かごにエントリがなければエラーを表示する。
   if '$data(%session.Data("basket",id)) {
     &JS<alert('個数を入力してください');>
     quit
   }
   // かごからエントリを削除
   kill %session.Data("basket",id)


この処理の

 if +amount=0 {
   // 個数の入力領域から入力された文字列を削除
   &JS<form.amount#(id)#.value = "";>

の部分は、javascriptからこのメソッドを呼び出す前にJavascript側で処理してください

以下の記述は、

&JS<top.menu.document.order.orderlist.length = #(count)#;>

以下のようにObjectScriptダイナミックオブジェクトに設定し、

set retune.orderlength = count

そしてjavascript側でtop.menu.document.order.orderlist.lengthに該当するjsonデータを設定してください

以下の記述は、

&JS<top.menu.document.order.orderlist[#(i-1)#].value= '#(id(i))#';
   top.menu.document.order.orderlist[#(i-1)#].text='#(line)#';>


まず、ObjectScriptのダイナミックオブジェクトとしてid(i)を配列形式で設定してください
lineも同様に処理してください
ObjectScriptの配列の基点(1から始まる)とJavascriptの配列の基点(0から始まる)の違いに注意してください

そしてJavascript側でjson形式で返ってきた配列の値を

top.menu.document.order.orderlist[n].value

に設定する処理を追加してください

同様に

top.menu.document.order.orderlist[n].textにjson形式の該当するデータを設定する処理を追加してください

元々のmain.cspの内容は、以下のとおりです。
 

<HTML>
<HEAD>
<TITLE>メイン</TITLE>
<STYLE type="text/css">
<!--
.editnumeric{
  text-align : right;
}
-->
</STYLE>
</HEAD>
<BODY>

<CSP:OBJECT NAME="cust" CLASSNAME="Shop.Customer" OBJID=#($get(%session.Data("oid")))#>

<SCRIPT LANGUAGE="JavaScript">
function addent(id,amount) {
  #server(..AddEntry(id,amount))#;
}
</SCRIPT>

<SCRIPT LANGUAGE="SQL" NAME="res">
 SELECT ID,Code,Name,Description,ListPrice FROM Shop.Product where deleteflg = 0
</SCRIPT>

<FORM name=form>

<TABLE cellpadding="0" cellspacing="2"><TBODY>
<CSP:WHILE CONDITION="res.Next()">
<TR><TD>
<TABLE border="1" cellspacing="0" cellpadding="0">
<TR><TD valign="middle" align="center">
<TABLE cellspacing="3" cellpadding="3" height="60">
<TBODY>
<TR>
<TD rowspan="2" width="100" align="center" valign="middle" nowrap>

<SCRIPT LANGUAGE="SQL" NAME="resimage" P1=#(res.GetData(2))#>
  SELECT Picture FROM Shop.Product where Code=?
</SCRIPT>
<csp:WHILE CONDITION="resimage.Next()">
<IMG SRC="_CSP.StreamServer.cls?STREAMOID=#(..Encrypt(resimage.GetData(1)))#&CONTENTTYPE=image/gif" width="100" height="100" border="0">
</csp:WHILE>
</TD>
<TD width="160"><B>#(res.GetData(3))#</B></TD>
<TD width="128"><FONT color="#333333">商品コード: #(res.GetData(2))#</FONT></TD>
<TD rowspan="2" align="center" width="71"><FONT color="#333333">注文数<BR>
</FONT><FONT color="#333333"><INPUT size="3" maxlength="3" type="text" name="amount#(res.GetData(1))#" class="editnumeric">個</FONT><BR>
<BR>
<INPUT type="button" value="かごへ" onclick="addent('#(res.GetData(1))#',form.amount#(res.GetData(1))#.value);"></TD>
</TR>
<TR>
<TD width="160"><FONT size="-1" color="#333333">#(res.GetData(4))#</FONT></TD>
<TD width="128" align="right"><FONT size="-1" face="MS Pゴシック" color="#CC0033"><I><FONT size="+3">#($fnumber(cust.determinePrice(res.GetData(5)),","))#</FONT></I> </FONT><FONT color="#CC0033">円</FONT></TD>
</TR>
</TBODY>
</TABLE>
</TD></TR></TABLE>
</TD></TR>

</CSP:WHILE>

</TBODY></TABLE>
</FORM>

<SCRIPT LANGUAGE=CACHE METHOD="AddEntry" ARGUMENTS="id:%String,amount:%Numeric">
  // 顧客情報の呼び出し
  if $get(%session.Data("oid"))="" {
    set cust=""
  } 
  else {
    set cust=##class(Shop.Customer).%OpenId(%session.Data("oid"))
  }
  if cust="" {
    &JS<alert('ログアウトされました。\n再度ログインしてください。');
    top.document.location = 'login.htm';>
	 	
    quit
  }

  // かごにエントリを追加
  if +amount=0 {
    // 個数の入力領域から入力された文字列を削除
    &JS<form.amount#(id)#.value = "";>
    // 個数が0の場合、かごにエントリがなければエラーを表示する。
    if '$data(%session.Data("basket",id)) {
      &JS<alert('個数を入力してください');>
      quit
    }
    // かごからエントリを削除
    kill %session.Data("basket",id)
	 	 
  } 
  else {
    // かごにエントリを追加
    set %session.Data("basket",id)=amount
  }
 
  //注文件数の取得
  set id="",count=0
  for  {
    set id=$order(%session.Data("basket",id))
    quit:id=""
    set id($increment(count))=id
  }
  // 注文件数から
  &JS<top.menu.document.order.orderlist.length = #(count)#;>
  set total=0
  for i=1:1:count {
    set oref=##class(Shop.Product).%OpenId(id(i))
    //set oref=##class(User.Product).%OpenId(id(i))
    set item=$extract(oref.Name,1,$zposition(oref.Name,20))
    set line=item_$justify("",20-$zwidth(item))_" "_$justify(%session.Data("basket",id(i)),3)
    &JS<top.menu.document.order.orderlist[#(i-1)#].value= '#(id(i))#';
    top.menu.document.order.orderlist[#(i-1)#].text='#(line)#';>
    set total=total+(cust.determinePrice(oref.ListPrice)*%session.Data("basket",id(i)))
  }
  &JS<top.menu.document.order.total.value = '#($fnumber(total,","))#';>
 
</SCRIPT>
</BODY>
</HTML>

生成したmain.cspは以下のとおりです。

<HTML>
<HEAD>
<TITLE>メイン</TITLE>
<STYLE type="text/css">
<!--
.editnumeric{
  text-align : right;
}
-->
</STYLE>
</HEAD>
<BODY>

<CSP:OBJECT NAME="cust" CLASSNAME="Shop.Customer" OBJID=#($get(%session.Data("oid")))#>

<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

<SCRIPT LANGUAGE="JavaScript">
function addent(id, amount) {
  // 個数が0または空の場合、入力欄をクリアしてからサーバーへ送信
  if (amount === "" || +amount === 0) {
    document.form.elements["amount" + id].value = "";
    amount = 0;
  }

  axios.post('http://localhost:8080/api/shop/addEntry', {
    id: id,
    amount: amount
  })
  .then(function(response) {
    var data = response.data;

    // ログアウト検知
    if (data.LogoutMessage) {
      alert(data.LogoutMessage);
      top.document.location = 'login.htm';
      return;
    }

    // 個数が0でかごにエントリがない場合のエラー
    if (data.ErrorMessage) {
      alert(data.ErrorMessage);
      return;
    }

    // 注文件数をメニューフレームのセレクトボックスに反映
    top.menu.document.order.orderlist.length = data.OrderLength;

    // 注文リストの各エントリを反映
    var ids = data.OrderIds;
    var lines = data.OrderLines;
    for (var n = 0; n < ids.length; n++) {
      top.menu.document.order.orderlist[n].value = ids[n];
      top.menu.document.order.orderlist[n].text  = lines[n];
    }

    // 合計金額を反映
    top.menu.document.order.total.value = data.Total;
  })
  .catch(function(error) {
    console.log(error);
  });
}
</SCRIPT>

<SCRIPT LANGUAGE="SQL" NAME="res">
 SELECT ID,Code,Name,Description,ListPrice FROM Shop.Product where deleteflg = 0
</SCRIPT>

<FORM name=form>

<TABLE cellpadding="0" cellspacing="2"><TBODY>
<CSP:WHILE CONDITION="res.Next()">
<TR><TD>
<TABLE border="1" cellspacing="0" cellpadding="0">
<TR><TD valign="middle" align="center">
<TABLE cellspacing="3" cellpadding="3" height="60">
<TBODY>
<TR>
<TD rowspan="2" width="100" align="center" valign="middle" nowrap>

<SCRIPT LANGUAGE="SQL" NAME="resimage" P1=#(res.GetData(2))#>
  SELECT Picture FROM Shop.Product where Code=?
</SCRIPT>
<csp:WHILE CONDITION="resimage.Next()">
<IMG SRC="_CSP.StreamServer.cls?STREAMOID=#(..Encrypt(resimage.GetData(1)))#&CONTENTTYPE=image/gif" width="100" height="100" border="0">
</csp:WHILE>
</TD>
<TD width="160"><B>#(res.GetData(3))#</B></TD>
<TD width="128"><FONT color="#333333">商品コード: #(res.GetData(2))#</FONT></TD>
<TD rowspan="2" align="center" width="71"><FONT color="#333333">注文数<BR>
</FONT><FONT color="#333333"><INPUT size="3" maxlength="3" type="text" name="amount#(res.GetData(1))#" class="editnumeric">個</FONT><BR>
<BR>
<INPUT type="button" value="かごへ" onclick="addent('#(res.GetData(1))#',form.elements['amount#(res.GetData(1))#'].value);"></TD>
</TR>
<TR>
<TD width="160"><FONT size="-1" color="#333333">#(res.GetData(4))#</FONT></TD>
<TD width="128" align="right"><FONT size="-1" face="MS Pゴシック" color="#CC0033"><I><FONT size="+3">#($fnumber(cust.determinePrice(res.GetData(5)),","))#</FONT></I> </FONT><FONT color="#CC0033">円</FONT></TD>
</TR>
</TBODY>
</TABLE>
</TD></TR></TABLE>
</TD></TR>

</CSP:WHILE>

</TBODY></TABLE>
</FORM>

</BODY>
</HTML>

そしてShop.APIクラスのコードは以下のようなものでした。


Class Shop.API Extends %CSP.REST
{
Parameter CONVERTINPUTSTREAM = 1;

XData UrlMap
{
<Routes>
 <Route Url="/addEntry" Method="POST" Call="AddEntry"/>
</Routes>
}

/// かごへのエントリ追加・削除を行い、注文リストをJSON形式で返す
ClassMethod AddEntry() As %Status
{
 set status = $$$OK

 try {

   if $data(%request) {
     set %response.ContentType = "application/json"
     set %response.CharSet = "utf-8"
   }

   set return = {}

   // リクエストボディ(JSON)の読み取り
   set body = ##class(%DynamicObject).%FromJSON(%request.Content)
   set id     = body.id
   set amount = body.amount

   // 顧客情報の呼び出し
   if $get(%session.Data("oid")) = "" {
     set cust = ""
   }
   else {
     set cust = ##class(Shop.Customer).%OpenId(%session.Data("oid"))
   }
   if cust = "" {
     set return.LogoutMessage = "ログアウトされました。\n再度ログインしてください。"
     write return.%ToJSON()
     quit
   }

   // かごにエントリを追加・削除
   if +amount = 0 {
     // 個数が0の場合、かごにエントリがなければエラーを返す
     if '$data(%session.Data("basket", id)) {
       set return.ErrorMessage = "個数を入力してください"
       write return.%ToJSON()
       quit
     }
     // かごからエントリを削除
     kill %session.Data("basket", id)
   }
   else {
     // かごにエントリを追加
     set %session.Data("basket", id) = amount
   }

   // 注文件数の取得
   set id = "", count = 0
   for {
     set id = $order(%session.Data("basket", id))
     quit:id=""
     set id($increment(count)) = id
   }

   // 注文件数をセット
   set return.OrderLength = count

   // 注文IDと表示行の配列をセット
   set orderIds  = []
   set orderLines = []
   set total = 0
   for i = 1:1:count {
     set oref = ##class(Shop.Product).%OpenId(id(i))
     set item = $extract(oref.Name, 1, $zposition(oref.Name, 20))
     set line = item _ $justify("", 20 - $zwidth(item)) _ " " _ $justify(%session.Data("basket", id(i)), 3)
     // ObjectScriptの配列は1基点 → Javascriptの0基点に合わせて末尾追加
     do orderIds.%Push(id(i))
     do orderLines.%Push(line)
     set total = total + (cust.determinePrice(oref.ListPrice) * %session.Data("basket", id(i)))
   }
   set return.OrderIds   = orderIds
   set return.OrderLines = orderLines
   set return.Total      = $fnumber(total, ",")

   write return.%ToJSON()

 }
 catch e {
   set status = e.AsStatus()
 }

 quit status
}

/// REST API定義の登録
ClassMethod CreateAPIDefinition() As %Status
{
   Set sc = $$$OK
   set namespace = $namespace
   set $namespace = "%SYS"
   set sec = ##class("Security.Applications").%New()
   set sec.Name        = "/api/shop"
   set sec.NameSpace   = namespace
   set sec.DispatchClass = "Shop.API"
   set sec.AutheEnabled = 96
   set sc = sec.%Save()
   set $namespace = namespace

   Return sc
}

}