全体構成
- HTML: 画面レイアウト(ヘッダ、プルダウン、パラメータ編集部、レスポンス表示部)。
- CSS: プルダウンの横並び、開閉可能なセクション、レスポンス表示の整形。
- JavaScript: プルダウンの動的生成、CSV/JS読み込み、APIリクエスト送信、レスポンス処理。
- Excel(VBA): 管理シートからプルダウン内容やパラメータをCSVまたはJSファイルに出力。
- 運用:管理シートに環境、機能、API、パラメータを定義。VBAでデータをdata.jsまたはdata.csvに出力。HTMLがdata.jsを読み込み、画面を動的に生成。ユーザーがプルダウンで選択し、パラメータを編集後、GET/POSTでAPIを呼び出し。
VBA
Sub ExportToJs()
Dim ws As Worksheet
Dim filePath As String
Dim row As Long, col As Long
Dim paramId As Long
Dim currentPaths As Object ' Collection
Dim arrayIndices As Object ' Dictionary
Dim output As String
filePath = ThisWorkbook.Path & "\data.js"
' 出力バッファを構築
output = "const data = {" & vbCrLf
' 環境
Set ws = ThisWorkbook.Sheets("Environment")
output = output & " environments: [" & vbCrLf
For row = 2 To ws.Cells(ws.Rows.Count, 1).End(xlUp).row
output = output & " { id: """ & ws.Cells(row, 1).Value & """, name: """ & ws.Cells(row, 2).Value & """, baseURL: """ & ws.Cells(row, 3).Value & """ }," & vbCrLf
Next row
output = output & " ]," & vbCrLf
' 機能
Set ws = ThisWorkbook.Sheets("Function")
output = output & " functions: [" & vbCrLf
For row = 2 To ws.Cells(ws.Rows.Count, 1).End(xlUp).row
output = output & " { id: """ & ws.Cells(row, 1).Value & """, name: """ & ws.Cells(row, 2).Value & """ }," & vbCrLf
Next row
output = output & " ]," & vbCrLf
' API
output = output & " apis: [" & vbCrLf
For Each ws In ThisWorkbook.Sheets
If ws.Name <> "Environment" And ws.Name <> "Function" Then
Dim apiId As String: apiId = ws.Range("D1").Value
Dim apiName As String: apiName = ws.Range("D2").Value
Dim functionId As String: functionId = Left(apiId, 8) ' 例: KLLLAA00
output = output & " { functionId: """ & functionId & """, id: """ & apiId & """, name: """ & apiName & """ }," & vbCrLf
End If
Next ws
output = output & " ]," & vbCrLf
' パラメータ
output = output & " parameters: [" & vbCrLf
For Each ws In ThisWorkbook.Sheets
If ws.Name <> "Environment" And ws.Name <> "Function" Then
apiId = ws.Range("D1").Value
apiName = ws.Range("D2").Value
functionId = Left(apiId, 8) ' 例: KLLLAA00
' パラメータ(BD列以降)
Dim paramCount As Long: paramCount = ws.Cells(4, 56).End(xlToRight).Column - 56 + 1 ' BD=56
For col = 56 To 55 + paramCount
paramId = paramId + 1
Dim paramIdStr As String: paramIdStr = Format(paramId, "000")
Dim paramName As String: paramName = ws.Cells(4, col).Value
If paramName = "" Then paramName = "パラメータ" & paramIdStr
' ネスト/配列構造を解析
Dim lastRow As Long: lastRow = ws.Cells(ws.Rows.Count, 3).End(xlUp).row
Set currentPaths = CreateObject("Scripting.Dictionary") ' 各要素はfullPathとitemType
Set arrayIndices = CreateObject("Scripting.Dictionary") ' 配列名 -> 次のインデックス
For row = 4 To lastRow
' 値がある最初の列(C=3~L=12)を特定
Dim level As Long: level = 3
While level <= 12 And ws.Cells(row, level).Value = ""
level = level + 1
Wend
If level > 12 Then GoTo NextRow
Dim depth As Long: depth = level - 3
Dim key As String: key = ws.Cells(row, level).Value
Dim itemType As String: itemType = ws.Cells(row, level + 23).Value ' AA列
' currentPathsをdepthに合わせて調整
While currentPaths.Count > depth
currentPaths.Remove currentPaths.Keys()(currentPaths.Count - 1)
Wend
' fullPathを構築
Dim fullPath As String
If currentPaths.Count = 0 Then
fullPath = key
If itemType = "配列" Then fullPath = fullPath & "[0]"
Else
Dim parent As Variant: parent = currentPaths.Item(currentPaths.Keys()(currentPaths.Count - 1))
Dim parentPath As String: parentPath = parent(0)
Dim parentType As String: parentType = parent(1)
If parentType = "配列" Then
Dim arrayName As String: arrayName = Split(parentPath, "[")(0)
If Not arrayIndices.Exists(arrayName) Then arrayIndices(arrayName) = 0
Dim index As Long: index = arrayIndices(arrayName)
fullPath = parentPath & "[" & index & "]"
If itemType = "配列" Then
fullPath = fullPath & "." & key & "[0]"
Else
fullPath = fullPath & "." & key
End If
arrayIndices(arrayName) = index + 1
Else
fullPath = parentPath & "." & key
If itemType = "配列" Then fullPath = fullPath & "[0]"
End If
End If
' currentPathsに追加(構造マーカー)
If itemType = "オブジェクト" Or itemType = "配列" Then
currentPaths(depth) = Array(fullPath, itemType)
End If
' プロパティ行の場合、パラメータを生成
If itemType = "" Then
Dim logicalName As String: logicalName = ws.Cells(row, level + 11).Value ' N列以降
Dim value As String: value = ws.Cells(row, col).Value
If value <> "" Then
output = output & " { apiId: """ & apiId & """, id: """ & paramIdStr & """, name: """ & paramName & """, logicalName: """ & logicalName & """, physicalName: """ & fullPath & """, value: """ & Replace(value, """", "\""") & """ }," & vbCrLf
End If
End If
NextRow:
Next row
Next col
End If
Next ws
output = output & " ]" & vbCrLf
output = output & "};"
' Shift_JISでファイル出力
With CreateObject("ADODB.Stream")
.Type = 2 ' テキスト
.Charset = "Shift_JIS"
.Open
.WriteText output
.SaveToFile filePath, 2 ' 上書き
.Close
End With
MsgBox "Exported to " & filePath
End Sub
HTML
index.html
<!DOCTYPE html>
<html>
<head>
<title>Web API Test Tool</title>
<meta charset="Shift_JIS">
<link rel="stylesheet" href="styles.css">
<script src="data.js"></script>
<script src="script.js"></script>
</head>
<body>
<!-- ヘッダ部分 -->
<div class="header">
<div class="dropdown">
<label>環境</label>
<select id="environment" onchange="updateURL()">
<option value="">選択してください</option>
</select>
</div>
<div class="dropdown">
<label>機能</label>
<select id="function" onchange="updateApis()">
<option value="">選択してください</option>
</select>
</div>
<div class="dropdown">
<label>API</label>
<select id="api" onchange="updateParameters()">
<option value="">選択してください</option>
</select>
</div>
<div class="dropdown">
<label>パラメータ</label>
<select id="parameter" onchange="displayParameterDetails()">
<option value="">選択してください</option>
</select>
</div>
</div>
<!-- リクエスト送信先URL -->
<div class="url-header">
<label>リクエスト送信先URL: </label>
<input type="text" id="requestUrl" oninput="updateCustomURL()">
</div>
<!-- リクエストヘッダ -->
<div class="section">
<h2 onclick="toggleSection('headers')">リクエストヘッダ <span id="headers-toggle">[+]</span></h2>
<div id="headers" class="collapsible">
<textarea id="requestHeaders" placeholder='{"Content-Type": "application/json"}'></textarea>
</div>
</div>
<!-- メソッド -->
<div class="method">
<label>メソッド: </label>
<select id="method">
<option value="GET">GET</option>
<option value="POST">POST</option>
</select>
<button onclick="sendRequest()">送信</button>
</div>
<!-- パラメータ表示部 -->
<div class="section">
<h2 onclick="toggleSection('parameters')">パラメータ <span id="parameters-toggle">[+]</span></h2>
<div id="parameters" class="collapsible">
<table>
<thead>
<tr>
<th>項目論理名</th>
<th>項目物理名</th>
<th>設定値</th>
</tr>
</thead>
<tbody id="parameterDetails"></tbody>
</table>
</div>
</div>
<!-- レスポンス表示部 -->
<div class="section">
<h2 onclick="toggleSection('response')">レスポンス <span id="response-toggle">[+]</span></h2>
<div id="response" class="collapsible">
<pre id="responseContent"></pre>
</div>
</div>
</body>
</html>
CSS
styles.html
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.header {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.dropdown {
display: flex;
align-items: center;
gap: 5px;
}
.dropdown label {
font-weight: bold;
}
.url-header, .method {
margin: 10px 0;
}
.url-header input {
width: 50%;
padding: 5px;
}
.section h2 {
cursor: pointer;
background: #f0f0f0;
padding: 10px;
margin: 10px 0;
}
.collapsible {
display: none;
padding: 10px;
}
.collapsible.open {
display: block;
}
#headers textarea {
width: 100%;
height: 50px; /* 狭くする */
resize: vertical;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ccc;
padding: 8px;
text-align: left;
}
th {
background: #f0f0f0;
}
input[type="text"] {
width: 100%;
box-sizing: border-box;
}
JS
script.js
// カスタムURLを保持
let customURL = '';
// プルダウン初期化
window.onload = () => {
initDropdowns();
updateURL();
};
// プルダウン初期化
function initDropdowns() {
const envSelect = document.getElementById('environment');
const funcSelect = document.getElementById('function');
// 環境(最初の項目をデフォルト選択)
data.environments.forEach((env, index) => {
const option = document.createElement('option');
option.value = env.id;
option.textContent = env.name;
envSelect.appendChild(option);
if (index === 0) {
envSelect.value = env.id; // デフォルト選択
}
});
// 機能
data.functions.forEach(func => {
const option = document.createElement('option');
option.value = func.id;
option.textContent = `${func.id}_${func.name}`;
funcSelect.appendChild(option);
});
updateApis(); // APIプルダウンを初期化
}
// APIプルダウン更新
function updateApis() {
const funcSelect = document.getElementById('function');
const apiSelect = document.getElementById('api');
apiSelect.innerHTML = '<option value="">選択してください</option>';
const funcId = funcSelect.value;
if (funcId) {
data.apis
.filter(api => api.functionId === funcId)
.forEach(api => {
const option = document.createElement('option');
option.value = api.id;
option.textContent = `${api.id}_${api.name}`;
apiSelect.appendChild(option);
});
}
updateParameters();
updateURL();
}
// パラメータプルダウン更新
function updateParameters() {
const apiSelect = document.getElementById('api');
const paramSelect = document.getElementById('parameter');
paramSelect.innerHTML = '<option value="">選択してください</option>';
const apiId = apiSelect.value;
if (apiId) {
const paramGroups = {};
data.parameters
.filter(param => param.apiId === apiId)
.forEach(param => {
if (!paramGroups[param.id]) {
paramGroups[param.id] = param.name;
}
});
Object.entries(paramGroups).forEach(([id, name]) => {
const option = document.createElement('option');
option.value = id;
option.textContent = `${id}_${name}`;
paramSelect.appendChild(option);
});
}
displayParameterDetails();
}
// パラメータ詳細表示
function displayParameterDetails() {
const apiSelect = document.getElementById('api');
const paramSelect = document.getElementById('parameter');
const paramDetails = document.getElementById('parameterDetails');
paramDetails.innerHTML = '';
const apiId = apiSelect.value;
const paramId = paramSelect.value;
if (apiId && paramId) {
data.parameters
.filter(param => param.apiId === apiId && param.id === paramId)
.forEach(param => {
if (param.value !== "") {
const row = document.createElement('tr');
row.innerHTML = `
<td>${param.logicalName}</td>
<td>${param.physicalName}</td>
<td><input type="text" value="${param.value}" onchange="updateParameterValue('${param.physicalName}', this.value)"></td>
`;
paramDetails.appendChild(row);
}
});
}
}
// パラメータ値更新
let editedParameters = {};
function updateParameterValue(physicalName, value) {
editedParameters[physicalName] = value;
}
// カスタムURL更新
function updateCustomURL() {
customURL = document.getElementById('requestUrl').value;
}
// リクエストURL更新
function updateURL() {
const envSelect = document.getElementById('environment');
const apiSelect = document.getElementById('api');
const requestUrl = document.getElementById('requestUrl');
const envId = envSelect.value;
const apiId = apiSelect.value;
if (envId && apiId) {
const env = data.environments.find(e => e.id === envId);
const defaultUrl = `${env.baseURL}${apiId}`;
requestUrl.value = customURL || defaultUrl; // カスタムURLがなければデフォルト
customURL = ''; // リセット
} else {
requestUrl.value = '';
}
}
// セクションの開閉
function toggleSection(sectionId) {
const section = document.getElementById(sectionId);
const toggle = document.getElementById(`${sectionId}-toggle`);
section.classList.toggle('open');
toggle.textContent = section.classList.contains('open') ? '[-]' : '[+]';
}
// リクエスト送信
function sendRequest() {
const envSelect = document.getElementById('environment');
const apiSelect = document.getElementById('api');
const methodSelect = document.getElementById('method');
const headersInput = document.getElementById('requestHeaders');
const requestUrl = document.getElementById('requestUrl');
const responseContent = document.getElementById('responseContent');
const envId = envSelect.value;
const apiId = apiSelect.value;
const method = methodSelect.value;
if (!envId || !apiId) {
responseContent.textContent = '環境とAPIを選択してください';
return;
}
const url = requestUrl.value; // 編集されたURLを使用
let headers = {};
try {
headers = JSON.parse(headersInput.value || '{}');
} catch (e) {
responseContent.textContent = '無効なヘッダ形式';
return;
}
// JSONボディ生成
const paramSelect = document.getElementById('parameter');
const paramId = paramSelect.value;
const body = {};
if (paramId) {
data.parameters
.filter(param => param.apiId === apiId && param.id === paramId)
.forEach(param => {
const value = editedParameters[param.physicalName] || param.value;
if (value !== "") {
let parsedValue = value;
if (value.startsWith('[') && value.endsWith(']')) {
try {
parsedValue = JSON.parse(value);
} catch (e) {
console.warn(`Invalid JSON array: ${value}`);
}
}
setNestedValue(body, param.physicalName, parsedValue);
}
});
}
// リクエスト送信
fetch(url, {
method: method,
headers: headers,
body: method === 'POST' ? JSON.stringify({ [apiId]: body }) : undefined
})
.then(response => {
responseContent.textContent = `Status: ${response.status}\n`;
return response.text();
})
.then(text => {
try {
const json = JSON.parse(text);
responseContent.textContent += JSON.stringify(json, null, 2);
} catch {
responseContent.textContent += text;
}
toggleSection('response');
})
.catch(error => {
responseContent.textContent = `エラー: ${error.message}`;
toggleSection('response');
});
}
// ネストされた値の設定
function setNestedValue(obj, key, value) {
const parts = key.split('.');
let current = obj;
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
const arrayMatch = part.match(/(\w+)\[(\d+)\]/);
if (arrayMatch) {
const arrayName = arrayMatch[1];
const index = parseInt(arrayMatch[2]);
current[arrayName] = current[arrayName] || [];
while (current[arrayName].length <= index) {
current[arrayName].push(null);
}
if (i === parts.length - 1) {
current[arrayName][index] = value;
} else {
current[arrayName][index] = current[arrayName][index] || {};
current = current[arrayName][index];
}
} else {
if (i === parts.length - 1) {
current[part] = value;
} else {
current[part] = current[part] || {};
current = current[part];
}
}
}
}