Open7
Unityクライアントにおける通信ハンドリング処理の実装(Firebase利用)

参考

実装
GPT-4o1と対話しながら作成してみた。
using Cysharp.Threading.Tasks;
using System.Threading;
/// <summary>
/// REST通信を行う共通インターフェース
/// </summary>
public interface IRequestSender
{
/// <summary>
/// REST APIリクエストを送信し、レスポンス文字列を返す。
/// 例: GET, POST, PUT, PATCH, DELETE に対応。
/// </summary>
/// <param name="method">HTTPメソッド</param>
/// <param name="url">URL</param>
/// <param name="data">POST/PUT/PATCH等のボディデータ</param>
/// <param name="maxRetries">最大リトライ回数</param>
/// <param name="initialInterval">リトライ時の初期インターバル(秒)</param>
/// <param name="timeoutSeconds">タイムアウト(秒)</param>
/// <param name="cancellationToken">キャンセル用トークン</param>
UniTask<string> SendRequestAsync(
HttpMethod method,
string url,
object data = null,
int maxRetries = 3,
float initialInterval = 2.0f,
float timeoutSeconds = 10.0f,
CancellationToken cancellationToken = default
);
/// <summary>
/// 進捗監視付きの大容量ダウンロード(GET想定)
/// </summary>
/// <param name="url">ダウンロード先URL</param>
/// <param name="noProgressTimeout">進捗が変化しない場合のタイムアウト秒数</param>
/// <param name="cancellationToken">キャンセル用トークン</param>
UniTask DownloadWithProgressAsync(
string url,
float noProgressTimeout = 5.0f,
CancellationToken cancellationToken = default
);
}
using Cysharp.Threading.Tasks;
using System.Threading;
/// <summary>
/// アクセストークン管理のインターフェース
/// </summary>
public interface ITokenManager
{
UniTask<string> GetAccessTokenAsync(CancellationToken cancellationToken = default);
}
/// <summary>
/// バージョン情報提供のインターフェース
/// </summary>
public interface IVersionProvider
{
string AppVersion { get; }
string MasterDataVersion { get; }
}
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
/// <summary>
/// 排他制御のインターフェース
/// </summary>
public interface IAsyncLock
{
UniTask<IDisposable> LockAsync(CancellationToken cancellationToken = default);
}
/// <summary>
/// HTTPメソッド (GET, POST, PUT, PATCH, DELETE)
/// </summary>
public enum HttpMethod
{
GET,
POST,
PUT,
PATCH,
DELETE
}
using System;
// カスタム例外クラス
public class NetworkException : Exception
{
public NetworkException(string message) : base(message) { }
}
public class VersionMismatchException : Exception
{
public VersionMismatchException(string message) : base(message) { }
}
public class MasterDataMismatchException : Exception
{
public MasterDataMismatchException(string message) : base(message) { }
}
public class DuplicateRequestException : Exception
{
public DuplicateRequestException(string message) : base(message) { }
}
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
// 排他制御クラス
public class AsyncLock : IAsyncLock
{
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
public async UniTask<IDisposable> LockAsync(CancellationToken cancellationToken = default)
{
await semaphore.WaitAsync(cancellationToken);
return new Releaser(semaphore);
}
private class Releaser : IDisposable
{
private SemaphoreSlim semaphore;
public Releaser(SemaphoreSlim semaphore)
{
this.semaphore = semaphore;
}
public void Dispose()
{
semaphore.Release();
}
}
}
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
// TokenManager (アクセストークン + 再認証 + 排他制御)
public class TokenManager : ITokenManager
{
private string accessToken;
private DateTime expirationTime;
private readonly AsyncLock asyncLock = new AsyncLock();
public async UniTask<string> GetAccessTokenAsync(CancellationToken cancellationToken = default)
{
if (string.IsNullOrEmpty(accessToken) || DateTime.UtcNow >= expirationTime)
{
await RefreshTokenIfNeededAsync(cancellationToken);
}
return accessToken;
}
private async UniTask RefreshTokenIfNeededAsync(CancellationToken cancellationToken)
{
using (await asyncLock.LockAsync())
{
// 他のリクエストが既に更新を済ませた場合のガード
if (!string.IsNullOrEmpty(accessToken) && DateTime.UtcNow < expirationTime)
{
return;
}
Debug.Log("Refreshing token...");
// 例: ログインAPI
string url = "https://example.com/api/login";
var loginData = new { username = "user", password = "pass" };
string jsonData = JsonUtility.ToJson(loginData);
using (UnityWebRequest request = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST))
{
request.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(jsonData));
request.downloadHandler = new DownloadHandlerBuffer();
request.SetRequestHeader("Content-Type", "application/json");
await request.SendWebRequest().ToUniTask(cancellationToken: cancellationToken);
if (request.result == UnityWebRequest.Result.ConnectionError ||
request.result == UnityWebRequest.Result.ProtocolError)
{
throw new NetworkException($"Failed to refresh token: {request.error}");
}
// レスポンスをパースしてtokenとexpiresInを取得する想定
var response = JsonUtility.FromJson<LoginResponse>(request.downloadHandler.text);
accessToken = response.token;
expirationTime = DateTime.UtcNow.AddSeconds(response.expiresIn);
Debug.Log("Token refreshed successfully.");
}
}
}
[Serializable]
private class LoginResponse
{
public string token;
public int expiresIn;
}
}
// VersionManager ( バージョン情報を提供)
public class VersionManager : IVersionProvider
{
public string AppVersion { get; } = "1.0.0";
public string MasterDataVersion { get; } = "20230101";
}
using System;
using System.Collections.Generic;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
// HttpApiClient
// すべてのRESTメソッドをまとめて扱う汎用クラス
// リトライ、タイムアウト、キャンセル、二重リクエスト防止、バージョンチェックなどを網羅
public class HttpApiClient : IRequestSender
{
private readonly ITokenManager tokenManager;
private readonly IVersionProvider versionProvider;
// 二重リクエスト防止用にIDを管理(本来はサーバ側で制御)
private readonly HashSet<string> processedRequestIds = new HashSet<string>();
public HttpApiClient(ITokenManager tokenManager, IVersionProvider versionProvider)
{
this.tokenManager = tokenManager;
this.versionProvider = versionProvider;
}
public async UniTask<string> SendRequestAsync(
HttpMethod method,
string url,
object data = null,
int maxRetries = 3,
float initialInterval = 2.0f,
float timeoutSeconds = 10.0f,
CancellationToken cancellationToken = default
)
{
// リトライのたびに同じIDを使うなら、外部から渡す設計もアリ。
// ここでは1回の呼び出しにつき1つ生成している例。
string requestId = Guid.NewGuid().ToString();
// 二重リクエストチェック(クライアント側での例示)
if (processedRequestIds.Contains(requestId))
{
throw new DuplicateRequestException($"Request [{requestId}] has already been processed.");
}
processedRequestIds.Add(requestId);
int retryCount = 0;
float interval = initialInterval;
while (retryCount <= maxRetries)
{
try
{
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
cts.CancelAfterSlim(TimeSpan.FromSeconds(timeoutSeconds));
// アクセストークン取得
string token = await tokenManager.GetAccessTokenAsync(cts.Token);
// UnityWebRequest生成
using (UnityWebRequest request = CreateUnityWebRequest(method, url, data))
{
// 共通ヘッダー(バージョン情報, リクエストID, アクセストークン等)
request.SetRequestHeader("App-Version", versionProvider.AppVersion);
request.SetRequestHeader("Master-Data-Version", versionProvider.MasterDataVersion);
request.SetRequestHeader("Request-Id", requestId);
request.SetRequestHeader("Authorization", $"Bearer {token}");
// 通信実行
await request.SendWebRequest().ToUniTask(cancellationToken: cts.Token);
// 通信エラー判定
if (request.result == UnityWebRequest.Result.ConnectionError ||
request.result == UnityWebRequest.Result.ProtocolError)
{
throw new NetworkException($"Request error: {request.error}");
}
// サーバーからバージョンエラーを表すステータスコードが返る想定
if (request.responseCode == 426) // 例: 426 Upgrade Required
{
throw new VersionMismatchException("App version mismatch. Please update the application.");
}
if (request.responseCode == 428) // 例: 428 Precondition Required
{
throw new MasterDataMismatchException("Master data version mismatch. Please refresh the data.");
}
// 成功
string responseText = request.downloadHandler.text;
Debug.Log($"[SendRequestAsync] {method} {url} -> {responseText}");
return responseText;
}
}
}
catch (OperationCanceledException)
{
// キャンセル or タイムアウト
Debug.LogWarning($"[SendRequestAsync] Request canceled or timed out. Method={method}, Url={url}");
throw;
}
catch (Exception ex) when (ShouldRetry(ex))
{
retryCount++;
if (retryCount > maxRetries)
{
Debug.LogError($"[SendRequestAsync] Max retries reached. Last error: {ex.Message}");
throw;
}
Debug.LogWarning($"[SendRequestAsync] Retrying {retryCount}/{maxRetries} in {interval:F1} seconds... : {ex.Message}");
await UniTask.Delay(TimeSpan.FromSeconds(interval), cancellationToken: cancellationToken);
interval *= 2; // 指数的にインターバルを伸ばす
}
catch (Exception ex)
{
Debug.LogError($"[SendRequestAsync] Request failed. Method={method}, Url={url}, Error={ex.Message}");
throw;
}
finally
{
processedRequestIds.Remove(requestId); // どんな場合でも削除
}
}
throw new Exception($"[SendRequestAsync] Request failed after all retries. Method={method}, Url={url}");
}
/// <summary>
/// 大容量ダウンロード + 進捗監視
/// </summary>
public async UniTask DownloadWithProgressAsync(
string url,
float noProgressTimeout = 5.0f,
CancellationToken cancellationToken = default
)
{
using (UnityWebRequest request = UnityWebRequest.Get(url))
{
float lastProgress = 0f;
float lastProgressTime = Time.time;
request.downloadHandler = new DownloadHandlerBuffer();
var sendOp = request.SendWebRequest();
while (!sendOp.isDone)
{
if (cancellationToken.IsCancellationRequested)
{
Debug.LogWarning("[DownloadWithProgressAsync] Canceled by user.");
throw new OperationCanceledException(cancellationToken);
}
// 進捗が増えたら時間を更新
if (request.downloadProgress > lastProgress)
{
lastProgress = request.downloadProgress;
lastProgressTime = Time.time;
Debug.Log($"Downloading... {(lastProgress * 100f):F1}%");
}
// 一定時間進捗が変化しない場合タイムアウト
else if (Time.time - lastProgressTime > noProgressTimeout)
{
throw new TimeoutException($"Download no progress for {noProgressTimeout}s. Aborting.");
}
await UniTask.Yield();
}
if (request.result == UnityWebRequest.Result.ConnectionError ||
request.result == UnityWebRequest.Result.ProtocolError)
{
throw new NetworkException($"[DownloadWithProgressAsync] Download failed: {request.error}");
}
Debug.Log("[DownloadWithProgressAsync] Download completed successfully.");
}
}
/// <summary>
/// HttpMethodとデータに応じてUnityWebRequestを生成
/// </summary>
private UnityWebRequest CreateUnityWebRequest(HttpMethod method, string url, object data)
{
// 本来は multipart や form-data 等も考慮が必要だが、ここでは簡易化
switch (method)
{
case HttpMethod.GET:
return UnityWebRequest.Get(url);
case HttpMethod.POST:
{
var req = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPOST);
if (data != null)
{
string json = JsonUtility.ToJson(data);
req.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json));
req.SetRequestHeader("Content-Type", "application/json");
}
req.downloadHandler = new DownloadHandlerBuffer();
return req;
}
case HttpMethod.PUT:
{
var req = new UnityWebRequest(url, UnityWebRequest.kHttpVerbPUT);
if (data != null)
{
string json = JsonUtility.ToJson(data);
req.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json));
req.SetRequestHeader("Content-Type", "application/json");
}
req.downloadHandler = new DownloadHandlerBuffer();
return req;
}
case HttpMethod.PATCH:
{
// UnityWebRequestにパッチ専用メソッドはないが、カスタムメソッドで対応
var req = new UnityWebRequest(url, "PATCH");
if (data != null)
{
string json = JsonUtility.ToJson(data);
req.uploadHandler = new UploadHandlerRaw(System.Text.Encoding.UTF8.GetBytes(json));
req.SetRequestHeader("Content-Type", "application/json");
}
req.downloadHandler = new DownloadHandlerBuffer();
return req;
}
case HttpMethod.DELETE:
{
var req = UnityWebRequest.Delete(url);
// Deleteでもボディを送る場合はカスタムメソッド化が必要
// (一部サーバーやライブラリがDELETE + bodyに対応していない可能性あり)
return req;
}
default:
throw new NotSupportedException($"Method {method} is not supported.");
}
}
/// <summary>
/// リトライ対象となる例外かどうか
/// </summary>
private bool ShouldRetry(Exception ex)
{
return ex is NetworkException;
}
}
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
// 7. 動作確認用サンプル
public class DemoRunner : MonoBehaviour
{
private IRequestSender apiClient;
private CancellationTokenSource cts;
private async void Start()
{
// 依存注入(SOLID: DIP, インターフェースに対して実装を注入)
ITokenManager tokenMgr = new TokenManager();
IVersionProvider versionMgr = new VersionManager();
apiClient = new HttpApiClient(tokenMgr, versionMgr);
cts = new CancellationTokenSource();
// 例: GETリクエスト
try
{
string url = "https://example.com/api/resource";
string response = await apiClient.SendRequestAsync(
HttpMethod.GET,
url,
data: null, // GETなので通常ボディ不要
maxRetries: 3,
initialInterval: 2f,
timeoutSeconds: 10f,
cancellationToken: cts.Token
);
Debug.Log($"[DemoRunner] GET Response: {response}");
}
catch (Exception ex)
{
Debug.LogError($"[DemoRunner] GET failed: {ex.Message}");
}
// 例: POSTリクエスト
try
{
string url = "https://example.com/api/create";
var postData = new { title = "Hello", body = "World" };
string response = await apiClient.SendRequestAsync(
HttpMethod.POST,
url,
data: postData,
maxRetries: 3,
initialInterval: 2f,
timeoutSeconds: 10f,
cancellationToken: cts.Token
);
Debug.Log($"[DemoRunner] POST Response: {response}");
}
catch (Exception ex)
{
Debug.LogError($"[DemoRunner] POST failed: {ex.Message}");
}
// 例: 大容量ファイルダウンロード(進捗監視付き)
/*
try
{
string downloadUrl = "https://example.com/largefile.zip";
await apiClient.DownloadWithProgressAsync(
downloadUrl,
noProgressTimeout: 5f,
cancellationToken: cts.Token
);
}
catch (Exception ex)
{
Debug.LogError($"[DemoRunner] Download failed: {ex.Message}");
}
*/
}
private void Update()
{
// キャンセルしたい場合、Escキーなどでキャンセル
if (Input.GetKeyDown(KeyCode.Escape))
{
Debug.Log("[DemoRunner] Canceling all requests...");
cts.Cancel();
}
}
}

Firebase認証の場合のトークン管理
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Firebase.Auth;
using UnityEngine;
public class FirebaseTokenManager : ITokenManager
{
private string idToken;
private DateTime expirationTime;
private readonly FirebaseAuth firebaseAuth;
private readonly AsyncLock asyncLock = new();
public FirebaseTokenManager()
{
// FirebaseAuth.DefaultInstance は FirebaseAuthManager.InitializeAsync() によって初期化済みであることが前提
firebaseAuth = FirebaseAuth.DefaultInstance;
}
public async UniTask<string> GetAccessTokenAsync(CancellationToken ct)
{
// IDトークンが期限切れの場合のみ更新
if (string.IsNullOrEmpty(idToken) || DateTime.UtcNow >= expirationTime)
{
await RefreshTokenIfNeededAsync(ct);
}
return idToken;
}
private async UniTask RefreshTokenIfNeededAsync(CancellationToken ct)
{
using (await asyncLock.LockAsync(ct))
{
// 他のリクエストでトークンが更新済みか確認
if (!string.IsNullOrEmpty(idToken) && DateTime.UtcNow < expirationTime)
{
return;
}
Debug.Log("Refreshing Firebase ID token...");
FirebaseUser user = firebaseAuth.CurrentUser;
if (user == null)
{
throw new NetworkException("No authenticated user. Please sign in first.");
}
try
{
idToken = await user.TokenAsync(true).AsUniTask().AttachExternalCancellation(ct);
// Firebase の ID トークンは通常 60 分有効。少し早めに更新するため 55 分とする
expirationTime = DateTime.UtcNow.AddMinutes(55);
Debug.Log("Firebase ID Token refreshed successfully.");
}
catch (Exception ex)
{
throw new NetworkException($"Failed to refresh Firebase ID Token: {ex.Message}");
}
}
}
}

Firebaseによるユーザ登録/ログイン
IFirebaseAuthManager インターフェース
using System.Threading;
using Cysharp.Threading.Tasks;
using Firebase.Auth;
/// <summary>
/// Firebase 認証マネージャが実装するインターフェース
/// </summary>
public interface IFirebaseAuthManager
{
// Firebase Authentication のインスタンス
FirebaseAuth Auth { get; }
// 現在サインインしているユーザ
FirebaseUser CurrentUser { get; }
// Firebase の依存関係をチェックし、初期化する
UniTask InitializeAsync(CancellationToken cancellationToken = default);
// メールアドレスとパスワードでユーザ登録を行う
UniTask RegisterUserAsync(string email, string password, CancellationToken cancellationToken = default);
// メールアドレスとパスワードでユーザのサインインを行う
UniTask SignInUserAsync(string email, string password, CancellationToken cancellationToken = default);
}
FirebaseAuthManager クラス
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using Firebase;
using Firebase.Auth;
using UnityEngine;
/// <summary>
/// Firebase 認証マネージャ
/// </summary>
public class FirebaseAuthManager : IFirebaseAuthManager
{
public FirebaseAuth Auth { get; private set; }
public FirebaseUser CurrentUser { get; private set; }
// コンストラクタ
public FirebaseAuthManager() { }
// Firebase の依存関係をチェックし、初期化する
public async UniTask InitializeAsync(CancellationToken cancellationToken = default)
{
var dependencyStatus = await FirebaseApp.CheckAndFixDependenciesAsync()
.WithCancellation(cancellationToken);
if (dependencyStatus == DependencyStatus.Available)
{
Debug.Log("Firebase dependencies are available.");
Auth = FirebaseAuth.DefaultInstance;
}
else
{
Debug.LogError($"Could not resolve all Firebase dependencies: {dependencyStatus}");
throw new Exception($"Firebase dependencies are not available: {dependencyStatus}");
}
}
// メールアドレスとパスワードでユーザ登録を行う
public async UniTask RegisterUserAsync(string email, string password, CancellationToken cancellationToken = default)
{
try
{
FirebaseUser newUser = await Auth.CreateUserWithEmailAndPasswordAsync(email, password)
.WithCancellation(cancellationToken);
Debug.Log($"User registered: {newUser.Email}");
CurrentUser = newUser;
}
catch (Exception ex)
{
Debug.LogError($"Registration failed: {ex.Message}");
throw;
}
}
// メールアドレスとパスワードでユーザのサインインを行う
public async UniTask SignInUserAsync(string email, string password, CancellationToken cancellationToken = default)
{
try
{
FirebaseUser signedInUser = await Auth.SignInWithEmailAndPasswordAsync(email, password)
.WithCancellation(cancellationToken);
Debug.Log($"User signed in: {signedInUser.Email}");
CurrentUser = signedInUser;
}
catch (Exception ex)
{
Debug.LogError($"Sign in failed: {ex.Message}");
throw;
}
}
}

Firebase AuthenticationによるREST API連携
クライアント側
- Firebase Authentication SDK を用いてユーザーがログイン(ソーシャル認証やメール/パスワード認証など)
- ログイン後、firebase.auth().currentUser.getIdToken() で ID トークンを取得する
- API 呼び出し時、HTTP リクエストの Authorization ヘッダに Bearer <idToken> を付与して送信する
サーバ側(REST API)
- リクエストを受信したら、HTTP ヘッダから Authorization 値を取り出す
- Bearer の後に続く文字列を Firebase の Admin SDK や JWT ライブラリを用いて検証する
- 例えば、Firebase Admin SDK の verifyIdToken() を利用すれば、トークンの署名検証や有効期限の確認が自動で行われる
- 検証に成功すれば、ユーザーの UID やその他の認証情報を利用してリクエスト処理を続行、失敗すれば 401 Unauthorized を返す

FirebaseAuthManagerの検証
動作した
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using Firebase.Auth;
public class FirebaseAuthTester : MonoBehaviour
{
private IFirebaseAuthManager firebaseAuthManager;
private CancellationTokenSource cts;
private async void Start()
{
cts = new CancellationTokenSource();
// Firebase の初期化
firebaseAuthManager = new FirebaseAuthManager();
try
{
await firebaseAuthManager.InitializeAsync(cts.Token);
}
catch (Exception ex)
{
Debug.LogError($"[FirebaseAuthTester] Firebase initialization failed: {ex.Message}");
return;
}
// サインインの実行(メール・パスワードは適宜置き換えてください)
try
{
await firebaseAuthManager.SignInUserAsync("your_email@example.com", "your_password", cts.Token);
Debug.Log("[FirebaseAuthTester] Firebase sign in successful");
}
catch (Exception ex)
{
Debug.LogError($"[FirebaseAuthTester] Firebase sign in failed: {ex.Message}");
return;
}
}
private void OnDestroy()
{
cts?.Cancel();
cts?.Dispose();
}
}

APIリクエストの検証
動作した
Firebase Cloud Functions側
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const express = require('express');
const bodyParser = require('body-parser');
admin.initializeApp();
// 定数として必要なバージョン情報とマスターデータ(実際はDBなどで管理)
const REQUIRED_APP_VERSION = "1.0.0";
const CURRENT_MASTER_DATA_VERSION = "1.0.0";
const mockMasterData = {
config: "Default configuration",
items: ["item1", "item2", "item3"]
};
// シンプルなメモリ上のDB(永続化はしません)
let mockDatabase = {};
// Express アプリケーションの作成
const app = express();
app.use(bodyParser.json());
/**
* エラーレスポンスを返すためのヘルパー関数
*/
const sendError = (res, status, message, extra = {}) => {
res.status(status).json({ error: message, ...extra });
};
/**
* 認証ミドルウェア
* - Authorization ヘッダーの存在チェック
* - Firebase の ID トークン検証
*/
const authMiddleware = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return sendError(res, 403, 'Unauthorized: No token provided.');
}
const idToken = authHeader.split('Bearer ')[1];
try {
await admin.auth().verifyIdToken(idToken);
next();
} catch (error) {
return sendError(res, 403, `Unauthorized: ${error.message}`);
}
};
/**
* バージョンチェックミドルウェア
* - クライアント送信の "app-version" と "master-data-version" の検証
*/
const versionCheckMiddleware = (req, res, next) => {
const clientAppVersion = req.headers["app-version"];
const clientMasterDataVersion = req.headers["master-data-version"];
if (clientAppVersion !== REQUIRED_APP_VERSION) {
return res.status(426).json({
error: "App version outdated",
requiredAppVersion: REQUIRED_APP_VERSION
});
}
if (clientMasterDataVersion !== CURRENT_MASTER_DATA_VERSION) {
return res.status(428).json({
error: "Master data version mismatch",
currentMasterDataVersion: CURRENT_MASTER_DATA_VERSION,
masterData: mockMasterData
});
}
next();
};
/**
* バージョン情報エンドポイント
* - 認証通過後に、最新のバージョン情報とマスターデータを提供
*/
app.get('/version', authMiddleware, (req, res) => {
res.status(200).json({
appVersion: REQUIRED_APP_VERSION,
masterDataVersion: CURRENT_MASTER_DATA_VERSION,
masterData: mockMasterData
});
});
// 認証ミドルウェアは /version 以外のエンドポイントにも適用
app.use(authMiddleware);
// バージョンチェックは認証後、/version以外に適用
app.use(versionCheckMiddleware);
/**
* CRUD 操作用のルートハンドラ
*/
app.all('/', async (req, res) => {
try {
switch (req.method) {
case 'GET':
handleGet(req, res);
break;
case 'POST':
handlePost(req, res);
break;
case 'PUT':
case 'PATCH':
handleUpdate(req, res);
break;
case 'DELETE':
handleDelete(req, res);
break;
default:
res.status(405).send("Method not allowed");
}
} catch (error) {
console.error("Error processing request:", error);
sendError(res, 500, "Internal Server Error");
}
});
/**
* GET メソッドの処理
*/
const handleGet = (req, res) => {
const id = req.query.id;
if (id) {
const record = mockDatabase[id];
if (record) {
res.status(200).json({ action: "read", id: id, data: record });
} else {
sendError(res, 404, "Record not found");
}
} else {
res.status(200).json({ action: "read_all", data: mockDatabase });
}
};
/**
* POST メソッドの処理(新規作成)
*/
const handlePost = (req, res) => {
const newRecord = req.body;
if (!newRecord || Object.keys(newRecord).length === 0) {
return sendError(res, 400, "Invalid request body");
}
const id = Date.now().toString(); // 実際は UUID などの利用が望ましい
mockDatabase[id] = newRecord;
res.status(201).json({ action: "create", id: id, data: newRecord });
};
/**
* PUT/PATCH メソッドの処理(更新)
*/
const handleUpdate = (req, res) => {
const id = req.query.id;
if (!id) {
return sendError(res, 400, "Missing id for update");
}
if (!mockDatabase[id]) {
return sendError(res, 404, "Record not found for update");
}
const updatedData = req.body;
mockDatabase[id] = updatedData;
res.status(200).json({ action: "update", id: id, data: updatedData });
};
/**
* DELETE メソッドの処理
*/
const handleDelete = (req, res) => {
const id = req.query.id;
if (!id) {
return sendError(res, 400, "Missing id for delete");
}
if (!mockDatabase[id]) {
return sendError(res, 404, "Record not found for delete");
}
delete mockDatabase[id];
res.status(200).json({ action: "delete", id: id });
};
exports.crudApi = functions.https.onRequest(app);
Unity側
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
[Serializable]
public class CrudResponse
{
public string action;
public string id;
public string data;
}
public class DemoRunner : MonoBehaviour
{
private IRequestSender _apiClient;
private CancellationTokenSource _cts;
private IFirebaseAuthManager _firebaseAuthManager;
// Cloud Functions の CRUD API エンドポイント(適宜変更)
private const string BaseUrl = "https://<your-region>-<your-project-id>.cloudfunctions.net/crudApi";
private async void Start()
{
_cts = new CancellationTokenSource();
try
{
// Firebase の初期化とサインインを実施
await InitializeFirebaseAsync();
}
catch (Exception ex)
{
Debug.LogError($"[CrudDemoRunner] Firebase 初期化またはサインインに失敗しました: {ex.Message}");
return;
}
// CRUD 操作の実行
await ExecuteCrudOperationsAsync();
}
/// <summary>
/// Firebase の初期化とユーザ認証、HttpApiClient の依存注入を行います。
/// </summary>
private async UniTask InitializeFirebaseAsync()
{
_firebaseAuthManager = new FirebaseAuthManager();
try
{
await _firebaseAuthManager.InitializeAsync(_cts.Token);
await _firebaseAuthManager.SignInUserAsync("your_email@example.com", "your_password", _cts.Token);
}
catch (Exception ex)
{
Debug.LogError($"[CrudDemoRunner] Firebase 認証エラー: {ex.Message}");
throw;
}
// 認証後に HttpApiClient の依存注入
ITokenManager tokenMgr = new FirebaseTokenManager();
IVersionProvider versionMgr = new VersionManager();
_apiClient = new HttpApiClient(tokenMgr, versionMgr);
}
/// <summary>
/// CRUD の各操作を順次実行します。
/// </summary>
private async UniTask ExecuteCrudOperationsAsync()
{
try
{
// Create: 新規レコード作成
string createResponse = await CreateRecordAsync(new { title = "Hello", body = "World" });
Debug.Log("[CrudDemoRunner] Create Response: " + createResponse);
CrudResponse createResult = JsonUtility.FromJson<CrudResponse>(createResponse);
string recordId = createResult.id;
Debug.Log("[CrudDemoRunner] Created record id: " + recordId);
// Read: 作成したレコードを取得
string readResponse = await ReadRecordAsync(recordId);
Debug.Log("[CrudDemoRunner] Read Response: " + readResponse);
// Update: レコードの更新
string updateResponse = await UpdateRecordAsync(recordId, new { title = "Updated", body = "Content" });
Debug.Log("[CrudDemoRunner] Update Response: " + updateResponse);
// Delete: レコードの削除
string deleteResponse = await DeleteRecordAsync(recordId);
Debug.Log("[CrudDemoRunner] Delete Response: " + deleteResponse);
// Read All: 全レコードの取得(削除後の確認)
string readAllResponse = await ReadAllRecordsAsync();
Debug.Log("[CrudDemoRunner] Read All Response: " + readAllResponse);
}
catch (Exception ex)
{
Debug.LogError($"[CrudDemoRunner] CRUD 操作中にエラーが発生しました: {ex.Message}");
}
}
/// <summary>
/// POST リクエストにより新規レコードを作成します。
/// </summary>
private async UniTask<string> CreateRecordAsync(object data)
{
return await _apiClient.SendRequestAsync(
HttpMethod.POST,
BaseUrl,
data,
maxRetries: 3,
initialInterval: 2f,
timeoutSeconds: 10f,
cancellationToken: _cts.Token
);
}
/// <summary>
/// GET リクエストにより特定のレコードを取得します。
/// </summary>
private async UniTask<string> ReadRecordAsync(string id)
{
string url = $"{BaseUrl}?id={id}";
return await _apiClient.SendRequestAsync(
HttpMethod.GET,
url,
data: null,
maxRetries: 3,
initialInterval: 2f,
timeoutSeconds: 10f,
cancellationToken: _cts.Token
);
}
/// <summary>
/// PUT リクエストにより特定のレコードを更新します。
/// </summary>
private async UniTask<string> UpdateRecordAsync(string id, object data)
{
string url = $"{BaseUrl}?id={id}";
return await _apiClient.SendRequestAsync(
HttpMethod.PUT,
url,
data,
maxRetries: 3,
initialInterval: 2f,
timeoutSeconds: 10f,
cancellationToken: _cts.Token
);
}
/// <summary>
/// DELETE リクエストにより特定のレコードを削除します。
/// </summary>
private async UniTask<string> DeleteRecordAsync(string id)
{
string url = $"{BaseUrl}?id={id}";
return await _apiClient.SendRequestAsync(
HttpMethod.DELETE,
url,
data: null,
maxRetries: 3,
initialInterval: 2f,
timeoutSeconds: 10f,
cancellationToken: _cts.Token
);
}
/// <summary>
/// GET リクエストにより全レコードを取得します。
/// </summary>
private async UniTask<string> ReadAllRecordsAsync()
{
return await _apiClient.SendRequestAsync(
HttpMethod.GET,
BaseUrl,
data: null,
maxRetries: 3,
initialInterval: 2f,
timeoutSeconds: 10f,
cancellationToken: _cts.Token
);
}
private void Update()
{
// Escape キーで全リクエストのキャンセルを実行
if (Input.GetKeyDown(KeyCode.Escape))
{
Debug.Log("[CrudDemoRunner] キャンセル要求を受信しました。");
_cts.Cancel();
}
}
private void OnDestroy()
{
_cts?.Cancel();
_cts?.Dispose();
}
}
作成者以外のコメントは許可されていません