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
}
}
ディスカッション (0)0