npm package: @g1cloud/api-gen
Description: CLI tool to generate OpenAPI v3 YAML from Spring Boot Java source code
Author: G1Cloud
Latest version: 1.2.0

# @g1cloud/api-gen

Spring Boot Java 소스 코드에서 OpenAPI v3 YAML 스펙을 생성하는 CLI 도구입니다.

## 기능

- **Java 소스 파싱**: `java-parser`를 사용하여 Java 파일에서 클래스 및 메서드 정보 추출
- **REST Controller 분석**: `@RestController`, `@Controller` 및 HTTP 매핑 어노테이션 감지
- **파라미터 추출**: `@RequestParam`, `@PathVariable`, `@RequestHeader`, `@RequestBody` 지원
- **응답 분석**: 반환 타입 추출 및 응답 스키마 생성
- **보안 어노테이션 파싱**: `@PreAuthorize`, `@PostAuthorize`, `@Secured` 어노테이션 분석
- **SpEL 표현식 파싱**: Spring Security 표현식에서 역할(role) 및 권한(authority) 추출
- **DTO 스키마 생성**: 요청/응답 DTO에 대한 스키마 재귀적 생성
- **특수 타입 처리**:
  - `SearchParam`: 페이지네이션 파라미터 자동 추가 (offset, limit, filter, sort)
  - `PaginatedList<T>`: 페이지네이션 헤더 추가 (X-Total-Count, X-Offset, X-Limit)

## 설치

```bash
# 의존성 설치
pnpm install

# 프로젝트 빌드
pnpm build

# 또는 ts-node로 직접 실행
pnpm dev -- --source ./examples --output ./test-output.yaml
```

## 사용법

```bash
# 기본 사용법 - 단일 통합 YAML 파일 생성
npx spring-to-openapi --source ./src/main/java --output ./openapi.yaml

# 컨트롤러별 모드 - 각 컨트롤러마다 별도 YAML 파일 생성
npx spring-to-openapi --source ./src/main/java --out-dir ./api-docs

# 모든 옵션 사용
npx spring-to-openapi \
  --source ./src/main/java \
  --output ./openapi.yaml \
  --title "My API" \
  --api-version "1.0.0" \
  --base-path "/api"
```

### CLI 옵션

| 옵션 | 단축 | 설명 | 기본값 |
|--------|-------|-------------|---------|
| `--source` | `-s` | Java 소스 코드 디렉토리 경로 (여러 경로 지정 가능) | (필수) |
| `--output` | `-o` | 출력 YAML 파일 경로 (단일 통합 파일) | `openapi.yaml` |
| `--out-dir` | `-d` | 출력 디렉토리 경로 (컨트롤러별 YAML 생성) | (없음) |
| `--title` | `-t` | API 제목 | `API Documentation` |
| `--api-version` | | API 버전 | `1.0.0` |
| `--base-path` | `-b` | API 기본 경로 | (없음) |

> **참고**: `--output`과 `--out-dir`은 상호 배타적입니다. 단일 통합 파일은 `--output`을, 컨트롤러별 YAML 파일 생성은 `--out-dir`을 사용하세요.

## 예제

### 입력: Java Controller

```java
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping
    @PreAuthorize("hasRole('USER')")
    public ResponseEntity<PaginatedList<UserDTO>> searchUsers(SearchParam searchParam) {
        return ResponseEntity.ok(new PaginatedList<>());
    }

    @GetMapping("/{id}")
    @PreAuthorize("hasAnyRole('USER', 'ADMIN')")
    public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
        return ResponseEntity.ok(new UserDTO());
    }

    @PutMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') or (#id == authentication.principal.id and hasRole('USER'))")
    public ResponseEntity<UserDTO> updateUser(
            @PathVariable Long id,
            @Valid @RequestBody UpdateUserRequest request) {
        return ResponseEntity.ok(new UserDTO());
    }
}
```

### 출력: OpenAPI YAML

```yaml
openapi: 3.0.0
info:
  title: My API
  version: 1.0.0
paths:
  /api/users:
    get:
      summary: Search users
      operationId: user_searchUsers
      parameters:
        - name: offset
          in: query
          schema:
            type: integer
          description: Pagination offset
        - name: limit
          in: query
          schema:
            type: integer
          description: Pagination limit
        - name: filter
          in: query
          schema:
            $ref: '#/components/schemas/Filter'
        - name: sort
          in: query
          schema:
            $ref: '#/components/schemas/Sort'
      responses:
        '200':
          description: Successful response
          headers:
            X-Total-Count:
              schema:
                type: integer
              description: Total number of items
            X-Offset:
              schema:
                type: integer
              description: Current offset
            X-Limit:
              schema:
                type: integer
              description: Current limit
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PaginatedListUserDTO'
      security:
        - bearerAuth: []
      x-required-roles:
        - ROLE_USER

  /api/users/{id}:
    get:
      summary: Get user by ID
      operationId: user_getUserById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserDTO'
      security:
        - bearerAuth: []
      x-required-roles:
        - ROLE_USER
        - ROLE_ADMIN

    put:
      summary: Update user
      operationId: user_updateUser
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
            format: int64
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateUserRequest'
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserDTO'
      security:
        - bearerAuth: []
      x-required-roles:
        - ROLE_ADMIN
        - ROLE_USER
      x-security-expression: "hasRole('ADMIN') or (#id == authentication.principal.id and hasRole('USER'))"

components:
  schemas:
    UserDTO:
      type: object
      properties:
        id:
          type: integer
          format: int64
        firstName:
          type: string
          minLength: 2
          maxLength: 50
        lastName:
          type: string
          minLength: 2
          maxLength: 50
        email:
          type: string
      required:
        - firstName
        - lastName
        - email

    Filter:
      type: object
      description: Filter conditions
      additionalProperties: true

    Sort:
      type: object
      description: Sort conditions
      properties:
        field:
          type: string
        direction:
          type: string
          enum:
            - ASC
            - DESC

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
```

## 특수 타입 처리

### SearchParam

메서드 파라미터가 `SearchParam` 타입인 경우, 생성기가 자동으로 페이지네이션 쿼리 파라미터를 추가합니다:

- `offset` (integer) - 페이지네이션 오프셋
- `limit` (integer) - 페이지네이션 제한
- `filter` (object) - 필터 조건 (Filter 스키마 참조)
- `sort` (object) - 정렬 조건 (Sort 스키마 참조)

### PaginatedList<T>

메서드가 `PaginatedList<T>`를 반환하는 경우, 생성기가:

1. 응답에 페이지네이션 헤더를 추가합니다:
   - `X-Total-Count` - 전체 항목 수
   - `X-Offset` - 현재 오프셋
   - `X-Limit` - 현재 제한

2. 다음 구조의 `PaginatedListT` 스키마를 생성합니다:
   ```yaml
   PaginatedListUserDTO:
     type: object
     properties:
       items:
         type: array
         items:
           $ref: '#/components/schemas/UserDTO'
       total:
         type: integer
       offset:
         type: integer
       limit:
         type: integer
   ```

### 보안 표현식

생성기가 `@PreAuthorize` 및 `@PostAuthorize`의 SpEL 표현식을 파싱합니다:

- `hasRole('ADMIN')` 같은 단순 표현식 → `x-required-roles: [ROLE_ADMIN]`
- `hasAnyRole('USER', 'ADMIN')`의 다중 역할 → `x-required-roles: [ROLE_USER, ROLE_ADMIN]`
- 복잡한 표현식은 `x-security-expression`에 보존

## 유효성 검사 어노테이션

생성기가 DTO 필드의 다음 유효성 검사 어노테이션을 인식합니다:

| 어노테이션 | OpenAPI 매핑 |
|------------|-----------------|
| `@NotNull`, `@NotBlank`, `@NotEmpty` | `required: true` |
| `@Size(min, max)` | `minLength`, `maxLength` |
| `@Min(value)` | `minimum` |
| `@Max(value)` | `maximum` |
| `@Pattern(regexp)` | `pattern` |
| `@Email` | `pattern` (이메일 정규식) |

## 프로젝트 구조

```
api-gen/
├── src/
│   ├── index.ts                  # CLI 진입점
│   ├── parser/
│   │   ├── javaParser.ts         # Java 파일 파싱
│   │   └── astAnalyzer.ts        # AST 분석 유틸리티
│   ├── analyzer/
│   │   ├── controllerAnalyzer.ts # Controller 감지
│   │   ├── parameterAnalyzer.ts  # 파라미터 추출
│   │   ├── responseAnalyzer.ts   # 응답 분석
│   │   ├── securityAnalyzer.ts   # 보안 어노테이션 파싱
│   │   └── schemaGenerator.ts    # DTO 스키마 생성
│   ├── generator/
│   │   └── openapiGenerator.ts   # OpenAPI YAML 생성
│   └── types/
│       └── index.ts              # TypeScript 타입 정의
├── examples/                     # 샘플 Java 파일
├── package.json
├── tsconfig.json
└── README.md
```

## 개발

```bash
# 의존성 설치
pnpm install

# 개발 모드로 실행
pnpm dev -- --source ./examples

# 프로덕션 빌드
pnpm build

# 테스트 실행 (예제 파일 사용)
pnpm test
```

# 실제 사용 예시

```shell
pnpm start --source /git/g1cloud2/g1cloud-sales --output ./out/g1cloud-sales.yaml --title "G1cloud Sales API" --api-version "2.0.0-SNAPSHOT"

npx @redocly/cli build-docs ./out/g1cloud-sales.yaml -o ./out/g1cloud-sales.html
```

```shell
pnpm start --source /git/g1cloud2/g1cloud-sales \
    --api-source /git/g1cloud2/g1cloud-sales/g1cloud-sales-app \
    --output ./out/g1cloud-sales-app.yaml \
    --title "G1cloud Sales API" \
    --api-version "2.0.0-SNAPSHOT"

npx @redocly/cli build-docs \
    ./out/g1cloud-sales-app.yaml \
    -o ./out/g1cloud-sales-app.html
```

```shell
pnpm start --source /git/g1cloud2/g1cloud-sales-service \
    --api-source /git/g1cloud2/g1cloud-sales-service/module-basis \
    --output ./out/g1cloud-sales-basis.yaml \
    --title "G1cloud Sales Basis API" \
    --api-version "2.0.0-SNAPSHOT"

npx @redocly/cli build-docs \
    ./out/g1cloud-sales-basis.yaml \
    -o ./out/g1cloud-sales-basis.html
```

## 변경 이력

### v1.2.0

- `--source` 옵션 다중 경로 처리 개선: Commander 내장 variadic 옵션 사용으로 전환, 공백 구분 경로 지원 추가
- 개발 환경 개선: tsconfig에 Node.js 타입 선언 추가

### v1.1.0

- Java record 파싱 지원 추가
- `@g1cloud/api-gen`으로 실행 가능하도록 추가
- MCP 생성 추가
- Javadoc의 `@param`, `@return`을 description에 추가