Skip to content

API Reference

Makoto Horikawa edited this page Jul 9, 2025 · 1 revision

🌐 API リファレンス

GitHub Power ScouterのAPI使用方法とリファレンス。GitHub REST API v3の効率的な活用方法を詳しく解説します。

🚀 API概要

GitHub REST API v3 基本情報

項目 詳細
ベースURL https://api.github.com
認証 Personal Access Token
レート制限 未認証: 60req/h, 認証済み: 5000req/h
ドキュメント GitHub API Docs

使用エンドポイント

graph TB
    A[GitHub API] --> B[Users API]
    A --> C[Repositories API]
    A --> D[Events API]
    A --> E[Rate Limit API]
    
    B --> F[/users/{username}]
    C --> G[/users/{username}/repos]
    C --> H[/repos/{owner}/{repo}/languages]
    D --> I[/users/{username}/events/public]
    E --> J[/rate_limit]
Loading

🔧 API Client実装

基本的なAPIクライアント

export class GitHubApiClient {
  private baseUrl = 'https://api.github.com';
  private token?: string;
  
  constructor(token?: string) {
    this.token = token;
  }
  
  private getHeaders(): HeadersInit {
    const headers: HeadersInit = {
      'Accept': 'application/vnd.github.v3+json',
      'User-Agent': 'GitHub-Power-Scouter/2.0',
    };
    
    if (this.token) {
      headers['Authorization'] = `token ${this.token}`;
    }
    
    return headers;
  }
  
  private async request<T>(endpoint: string): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;
    const response = await fetch(url, {
      headers: this.getHeaders(),
    });
    
    if (!response.ok) {
      throw new ApiError(
        `API request failed: ${response.status} ${response.statusText}`,
        response.status,
        await response.json()
      );
    }
    
    return response.json();
  }
  
  // レート制限情報を取得
  async getRateLimit(): Promise<GitHubRateLimit> {
    return this.request<GitHubRateLimit>('/rate_limit');
  }
  
  // ユーザー情報を取得
  async getUser(username: string): Promise<GitHubUser> {
    return this.request<GitHubUser>(`/users/${username}`);
  }
  
  // ユーザーのリポジトリ一覧を取得
  async getUserRepos(username: string, options?: RepoOptions): Promise<GitHubRepo[]> {
    const params = new URLSearchParams({
      per_page: '100',
      sort: 'stars',
      direction: 'desc',
      ...options,
    });
    
    return this.request<GitHubRepo[]>(`/users/${username}/repos?${params}`);
  }
  
  // リポジトリの言語統計を取得
  async getRepoLanguages(owner: string, repo: string): Promise<LanguageStats> {
    return this.request<LanguageStats>(`/repos/${owner}/${repo}/languages`);
  }
  
  // ユーザーの公開イベントを取得
  async getUserEvents(username: string): Promise<GitHubEvent[]> {
    return this.request<GitHubEvent[]>(`/users/${username}/events/public?per_page=100`);
  }
  
  // リポジトリのコンテンツを取得
  async getRepoContents(owner: string, repo: string, path = ''): Promise<RepoContent[]> {
    return this.request<RepoContent[]>(`/repos/${owner}/${repo}/contents/${path}`);
  }
}

エラーハンドリング

export class ApiError extends Error {
  constructor(
    message: string,
    public status: number,
    public response?: any
  ) {
    super(message);
    this.name = 'ApiError';
  }
  
  // GitHub API固有エラーの判定
  isRateLimitError(): boolean {
    return this.status === 403 && this.response?.message?.includes('rate limit');
  }
  
  isNotFoundError(): boolean {
    return this.status === 404;
  }
  
  isUnauthorizedError(): boolean {
    return this.status === 401;
  }
  
  // ユーザーフレンドリーなエラーメッセージ
  getUserMessage(): string {
    switch (this.status) {
      case 404:
        return 'ユーザーまたはリポジトリが見つかりません';
      case 401:
        return 'GitHubトークンが無効です';
      case 403:
        if (this.isRateLimitError()) {
          return 'API制限に達しました。しばらく待ってから再試行してください';
        }
        return 'アクセスが拒否されました';
      case 500:
        return 'GitHubサーバーエラーが発生しました';
      default:
        return `予期しないエラーが発生しました (${this.status})`;
    }
  }
}

📊 データ処理・変換

パワーレベル計算

interface PowerLevelCalculator {
  calculateBasePower(userData: GitHubUser): number;
  calculateRepoBonus(repos: GitHubRepo[]): number;
  calculateLanguageBonus(languages: ProcessedLanguage[]): number;
  calculateActivityBonus(activity: ActivityData): number;
  calculateTotalPower(
    userData: GitHubUser,
    repos: GitHubRepo[],
    languages: ProcessedLanguage[],
    activity: ActivityData
  ): PowerLevelResult;
}

export class PowerLevelCalculator implements PowerLevelCalculator {
  // 基本パワーの計算
  calculateBasePower(userData: GitHubUser): number {
    const accountAge = this.getAccountAge(userData.created_at);
    
    return (
      userData.public_repos * 100 +      // リポジトリ数
      userData.followers * 30 +          // フォロワー数
      userData.following * 5 +           // フォロー数
      userData.public_gists * 50 +       // Gist数
      accountAge * 1000 +                // アカウント年齢
      (userData.bio ? 500 : 0) +         // プロフィール記入
      (userData.blog ? 500 : 0) +        // ブログ記入
      (userData.company ? 1000 : 0) +    // 会社情報
      (userData.location ? 300 : 0) +    // 場所情報
      (userData.hireable ? 2000 : 0)     // 採用可能
    );
  }
  
  // リポジトリボーナスの計算
  calculateRepoBonus(repos: GitHubRepo[]): number {
    let totalStars = 0;
    let totalForks = 0;
    let totalWatchers = 0;
    let originalRepos = 0;
    
    repos.forEach(repo => {
      if (!repo.fork) {
        originalRepos++;
        totalStars += repo.stargazers_count;
        totalForks += repo.forks_count;
        totalWatchers += repo.watchers_count;
      }
    });
    
    return (
      originalRepos * 200 +        // オリジナルリポジトリ
      totalStars * 50 +            // 獲得スター数
      totalForks * 40 +            // フォーク数
      totalWatchers * 20           // ウォッチャー数
    );
  }
  
  // 言語ボーナスの計算
  calculateLanguageBonus(languages: ProcessedLanguage[]): number {
    const diversityBonus = languages.length * 1000;
    const dominanceBonus = languages.length > 0 
      ? (languages[0].percentage > 80 ? 2000 : 0) 
      : 0;
    
    return diversityBonus + dominanceBonus;
  }
  
  // 活動ボーナスの計算
  calculateActivityBonus(activity: ActivityData): number {
    return (
      activity.commits * 10 +           // コミット数
      activity.pullRequests * 100 +     // PR数
      activity.issues * 50 +            // イシュー数
      activity.recentContributions * 20 // 最近の貢献
    );
  }
  
  // 総パワーレベル計算
  calculateTotalPower(
    userData: GitHubUser,
    repos: GitHubRepo[],
    languages: ProcessedLanguage[],
    activity: ActivityData
  ): PowerLevelResult {
    const basePower = this.calculateBasePower(userData);
    const repoBonus = this.calculateRepoBonus(repos);
    const languageBonus = this.calculateLanguageBonus(languages);
    const activityBonus = this.calculateActivityBonus(activity);
    
    const totalPower = basePower + repoBonus + languageBonus + activityBonus;
    
    return {
      power: Math.floor(totalPower),
      breakdown: {
        base: basePower,
        repo: repoBonus,
        language: languageBonus,
        activity: activityBonus,
      },
      stats: {
        repos: userData.public_repos,
        originalRepos: repos.filter(r => !r.fork).length,
        stars: repos.reduce((sum, r) => sum + r.stargazers_count, 0),
        forks: repos.reduce((sum, r) => sum + r.forks_count, 0),
        followers: userData.followers,
        following: userData.following,
        accountAge: this.getAccountAge(userData.created_at),
        languages,
        activity,
        gists: userData.public_gists,
      },
    };
  }
  
  private getAccountAge(createdAt: string): number {
    const created = new Date(createdAt);
    const now = new Date();
    return Math.floor((now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24 * 365));
  }
}

言語統計処理

export class LanguageAnalyzer {
  async analyzeUserLanguages(
    repos: GitHubRepo[],
    apiClient: GitHubApiClient
  ): Promise<ProcessedLanguage[]> {
    const languageStats: Record<string, number> = {};
    let totalBytes = 0;
    
    // 上位リポジトリの言語統計を取得
    const topRepos = repos
      .filter(repo => !repo.fork)
      .sort((a, b) => b.stargazers_count - a.stargazers_count)
      .slice(0, 10);
    
    for (const repo of topRepos) {
      try {
        const languages = await apiClient.getRepoLanguages(
          repo.owner.login,
          repo.name
        );
        
        Object.entries(languages).forEach(([lang, bytes]) => {
          languageStats[lang] = (languageStats[lang] || 0) + bytes;
          totalBytes += bytes;
        });
      } catch (error) {
        console.warn(`Failed to fetch languages for ${repo.full_name}:`, error);
      }
    }
    
    // 言語統計を処理
    return Object.entries(languageStats)
      .map(([name, bytes]) => ({
        name,
        bytes,
        percentage: Math.round((bytes / totalBytes) * 100),
      }))
      .sort((a, b) => b.bytes - a.bytes)
      .slice(0, 5); // 上位5言語
  }
  
  // 言語別の熟練度を推定
  estimateProficiency(language: ProcessedLanguage): 'beginner' | 'intermediate' | 'advanced' {
    if (language.percentage >= 50) return 'advanced';
    if (language.percentage >= 20) return 'intermediate';
    return 'beginner';
  }
  
  // 人気言語かどうかを判定
  isPopularLanguage(languageName: string): boolean {
    const popularLanguages = [
      'JavaScript', 'Python', 'Java', 'TypeScript', 'C#',
      'C++', 'PHP', 'Shell', 'C', 'Ruby', 'Go', 'Rust',
      'Swift', 'Kotlin', 'Scala', 'Dart', 'R', 'Matlab',
    ];
    return popularLanguages.includes(languageName);
  }
}

技術スタック分析

export class TechStackAnalyzer {
  async analyzeTechStack(
    repos: GitHubRepo[],
    apiClient: GitHubApiClient
  ): Promise<TechStack> {
    const techStack: TechStack = {
      languages: {},
      frameworks: new Set(),
      devops: new Set(),
      testing: new Set(),
      databases: new Set(),
      infrastructure: new Set(),
    };
    
    // 上位リポジトリを分析
    const topRepos = repos
      .filter(repo => !repo.fork)
      .sort((a, b) => b.stargazers_count - a.stargazers_count)
      .slice(0, 20);
    
    for (const repo of topRepos) {
      try {
        const contents = await apiClient.getRepoContents(
          repo.owner.login,
          repo.name
        );
        
        await this.analyzeRepoContents(contents, techStack, apiClient, repo);
      } catch (error) {
        console.warn(`Failed to analyze ${repo.full_name}:`, error);
      }
    }
    
    return techStack;
  }
  
  private async analyzeRepoContents(
    contents: RepoContent[],
    techStack: TechStack,
    apiClient: GitHubApiClient,
    repo: GitHubRepo
  ): Promise<void> {
    for (const item of contents) {
      const fileName = item.name.toLowerCase();
      
      // 設定ファイルから技術スタックを推定
      await this.analyzeConfigFiles(fileName, techStack, apiClient, repo);
      
      // ディレクトリ構造から技術スタックを推定
      if (item.type === 'dir') {
        this.analyzeDirectoryStructure(fileName, techStack);
      }
    }
  }
  
  private async analyzeConfigFiles(
    fileName: string,
    techStack: TechStack,
    apiClient: GitHubApiClient,
    repo: GitHubRepo
  ): Promise<void> {
    const configAnalyzers = {
      'package.json': () => this.analyzePackageJson(apiClient, repo, techStack),
      'requirements.txt': () => techStack.frameworks.add('Python'),
      'pyproject.toml': () => techStack.frameworks.add('Python (Modern)'),
      'composer.json': () => techStack.frameworks.add('PHP/Composer'),
      'pom.xml': () => techStack.frameworks.add('Java/Maven'),
      'build.gradle': () => techStack.frameworks.add('Java/Gradle'),
      'cargo.toml': () => techStack.frameworks.add('Rust'),
      'go.mod': () => techStack.frameworks.add('Go'),
      'gemfile': () => techStack.frameworks.add('Ruby'),
      'dockerfile': () => techStack.devops.add('Docker'),
    };
    
    const analyzer = configAnalyzers[fileName];
    if (analyzer) {
      await analyzer();
    }
    
    // YAML/YMLファイルの分析
    if (fileName.endsWith('.yml') || fileName.endsWith('.yaml')) {
      this.analyzeYamlFile(fileName, techStack);
    }
  }
  
  private async analyzePackageJson(
    apiClient: GitHubApiClient,
    repo: GitHubRepo,
    techStack: TechStack
  ): Promise<void> {
    try {
      const content = await apiClient.getRepoContents(
        repo.owner.login,
        repo.name,
        'package.json'
      );
      
      if (Array.isArray(content)) return;
      
      const packageData = JSON.parse(atob(content.content));
      const allDeps = {
        ...packageData.dependencies,
        ...packageData.devDependencies,
      };
      
      // フレームワーク検出
      const frameworks = {
        'react': 'React',
        'vue': 'Vue.js',
        '@angular/core': 'Angular',
        'next': 'Next.js',
        'nuxt': 'Nuxt.js',
        'express': 'Express.js',
        'fastify': 'Fastify',
        'nestjs': 'NestJS',
        'svelte': 'Svelte',
        'ember-source': 'Ember.js',
      };
      
      Object.entries(frameworks).forEach(([dep, name]) => {
        if (allDeps[dep]) {
          techStack.frameworks.add(name);
        }
      });
      
      // テストツール検出
      const testTools = {
        'jest': 'Jest',
        'mocha': 'Mocha',
        'cypress': 'Cypress',
        'playwright': 'Playwright',
        'vitest': 'Vitest',
        'testing-library': 'Testing Library',
      };
      
      Object.entries(testTools).forEach(([dep, name]) => {
        if (allDeps[dep] || Object.keys(allDeps).some(key => key.includes(dep))) {
          techStack.testing.add(name);
        }
      });
      
      // ビルドツール検出
      const buildTools = {
        'webpack': 'Webpack',
        'vite': 'Vite',
        'rollup': 'Rollup',
        'esbuild': 'ESBuild',
        'parcel': 'Parcel',
        'turbo': 'Turbo',
      };
      
      Object.entries(buildTools).forEach(([dep, name]) => {
        if (allDeps[dep]) {
          techStack.devops.add(name);
        }
      });
      
    } catch (error) {
      console.warn('Failed to analyze package.json:', error);
    }
  }
  
  private analyzeYamlFile(fileName: string, techStack: TechStack): void {
    if (fileName.includes('github')) {
      techStack.devops.add('GitHub Actions');
    } else if (fileName.includes('gitlab')) {
      techStack.devops.add('GitLab CI');
    } else if (fileName.includes('circle')) {
      techStack.devops.add('CircleCI');
    } else if (fileName.includes('travis')) {
      techStack.devops.add('Travis CI');
    } else if (fileName.includes('docker-compose')) {
      techStack.devops.add('Docker Compose');
    } else if (fileName.includes('kubernetes') || fileName.includes('k8s')) {
      techStack.infrastructure.add('Kubernetes');
    }
  }
  
  private analyzeDirectoryStructure(dirName: string, techStack: TechStack): void {
    const directories = {
      'test': () => techStack.testing.add('Test-Driven Development'),
      'tests': () => techStack.testing.add('Test-Driven Development'),
      'spec': () => techStack.testing.add('Test-Driven Development'),
      '__tests__': () => techStack.testing.add('Test-Driven Development'),
      'cypress': () => techStack.testing.add('Cypress'),
      'e2e': () => techStack.testing.add('End-to-End Testing'),
      'docker': () => techStack.devops.add('Docker'),
      'kubernetes': () => techStack.infrastructure.add('Kubernetes'),
      'terraform': () => techStack.infrastructure.add('Terraform'),
      'ansible': () => techStack.infrastructure.add('Ansible'),
    };
    
    const analyzer = directories[dirName];
    if (analyzer) {
      analyzer();
    }
  }
}

🔄 レート制限管理

レート制限ハンドリング

export class RateLimitManager {
  private lastRequestTime = 0;
  private requestCount = 0;
  private resetTime = 0;
  private remaining = 0;
  
  async checkRateLimit(apiClient: GitHubApiClient): Promise<void> {
    try {
      const rateLimit = await apiClient.getRateLimit();
      
      this.remaining = rateLimit.rate.remaining;
      this.resetTime = rateLimit.rate.reset * 1000;
      
      if (this.remaining === 0) {
        const waitTime = this.resetTime - Date.now();
        if (waitTime > 0) {
          throw new ApiError(
            `Rate limit exceeded. Reset in ${Math.ceil(waitTime / 1000)}s`,
            429,
            { reset_at: new Date(this.resetTime) }
          );
        }
      }
    } catch (error) {
      console.warn('Failed to check rate limit:', error);
    }
  }
  
  async throttleRequest<T>(
    request: () => Promise<T>,
    minInterval = 100
  ): Promise<T> {
    const now = Date.now();
    const timeSinceLastRequest = now - this.lastRequestTime;
    
    if (timeSinceLastRequest < minInterval) {
      await new Promise(resolve => 
        setTimeout(resolve, minInterval - timeSinceLastRequest)
      );
    }
    
    this.lastRequestTime = Date.now();
    this.requestCount++;
    
    try {
      return await request();
    } catch (error) {
      if (error instanceof ApiError && error.isRateLimitError()) {
        // 指数バックオフで再試行
        await this.exponentialBackoff();
        return this.throttleRequest(request, minInterval);
      }
      throw error;
    }
  }
  
  private async exponentialBackoff(attempt = 1): Promise<void> {
    const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
    await new Promise(resolve => setTimeout(resolve, delay));
  }
  
  getRemainingRequests(): number {
    return this.remaining;
  }
  
  getResetTime(): Date {
    return new Date(this.resetTime);
  }
}

🧪 テスト用モック

API レスポンスモック

export class MockGitHubApiClient implements GitHubApiClient {
  async getRateLimit(): Promise<GitHubRateLimit> {
    return {
      rate: {
        limit: 5000,
        remaining: 4999,
        reset: Math.floor(Date.now() / 1000) + 3600,
        used: 1,
      },
      search: {
        limit: 30,
        remaining: 30,
        reset: Math.floor(Date.now() / 1000) + 3600,
        used: 0,
      },
    };
  }
  
  async getUser(username: string): Promise<GitHubUser> {
    return {
      login: username,
      id: 1,
      avatar_url: `https://github.com/${username}.png`,
      html_url: `https://github.com/${username}`,
      name: `${username} (Mock)`,
      company: 'Mock Company',
      blog: 'https://example.com',
      location: 'Mock Location',
      email: null,
      hireable: true,
      bio: 'Mock user for testing',
      public_repos: 50,
      public_gists: 10,
      followers: 1000,
      following: 100,
      created_at: '2010-01-01T00:00:00Z',
      updated_at: '2024-01-01T00:00:00Z',
    };
  }
  
  async getUserRepos(username: string): Promise<GitHubRepo[]> {
    return [
      {
        id: 1,
        name: 'awesome-project',
        full_name: `${username}/awesome-project`,
        private: false,
        owner: {
          login: username,
          id: 1,
          avatar_url: `https://github.com/${username}.png`,
        },
        html_url: `https://github.com/${username}/awesome-project`,
        description: 'An awesome project',
        fork: false,
        url: `https://api.github.com/repos/${username}/awesome-project`,
        languages_url: `https://api.github.com/repos/${username}/awesome-project/languages`,
        stargazers_count: 100,
        watchers_count: 50,
        forks_count: 25,
        open_issues_count: 5,
        created_at: '2022-01-01T00:00:00Z',
        updated_at: '2024-01-01T00:00:00Z',
        pushed_at: '2024-01-01T00:00:00Z',
        language: 'TypeScript',
        size: 1024,
      },
    ];
  }
}

📚 使用例

完全な使用例

// メインアプリケーションでの使用
const useGitHubScouter = (token?: string) => {
  const apiClient = useMemo(() => new GitHubApiClient(token), [token]);
  const rateLimitManager = useMemo(() => new RateLimitManager(), []);
  const powerCalculator = useMemo(() => new PowerLevelCalculator(), []);
  const languageAnalyzer = useMemo(() => new LanguageAnalyzer(), []);
  const techStackAnalyzer = useMemo(() => new TechStackAnalyzer(), []);
  
  const scanUser = useCallback(async (username: string) => {
    try {
      // レート制限チェック
      await rateLimitManager.checkRateLimit(apiClient);
      
      // 並行でデータ取得
      const [userData, repos] = await Promise.all([
        rateLimitManager.throttleRequest(() => apiClient.getUser(username)),
        rateLimitManager.throttleRequest(() => apiClient.getUserRepos(username)),
      ]);
      
      // 詳細分析
      const [languages, events, techStack] = await Promise.all([
        languageAnalyzer.analyzeUserLanguages(repos, apiClient),
        rateLimitManager.throttleRequest(() => apiClient.getUserEvents(username)),
        techStackAnalyzer.analyzeTechStack(repos, apiClient),
      ]);
      
      // 活動データ処理
      const activity = processUserEvents(events);
      
      // パワーレベル計算
      const powerLevel = powerCalculator.calculateTotalPower(
        userData,
        repos,
        languages,
        activity
      );
      
      return {
        userData,
        repos,
        languages,
        techStack,
        activity,
        powerLevel,
      };
    } catch (error) {
      if (error instanceof ApiError) {
        throw error;
      }
      throw new Error(`Scan failed: ${error.message}`);
    }
  }, [apiClient, rateLimitManager, powerCalculator, languageAnalyzer, techStackAnalyzer]);
  
  return { scanUser };
};

🎯 このAPIリファレンスを参考に、効率的なGitHub APIの活用を行いましょう!

次はコンポーネントリファレンスでReactコンポーネントの詳細を確認できます。

Clone this wiki locally