internal/genmodel
외부 OpenAPI 문서 → Go HTTP 클라이언트 코드 생성기.
internal/gen/gogin의 Model 생성과는 역할이 다름 — 이 패키지는 "외부 서비스 연동용 클라이언트"를 만든다.
gogin의 Model 생성과의 차이
| 항목 |
genmodel |
gen/gogin |
| 입력 |
OpenAPI 문서 (파일/URL) |
DDL + sqlc 쿼리 + OpenAPI 인터페이스 |
| 산출 패키지 |
package external |
package model |
| 산출물 성격 |
HTTP 클라이언트 구현 |
DB 접근 구현 |
| 호출 대상 |
외부 HTTP 서비스 |
로컬 DB |
| 서로 연결 |
없음 (독립) |
없음 (독립) |
즉 Escrow, Payment 같은 외부 서비스의 OpenAPI를 받아서 Go에서 쓸 수 있는 클라이언트(EscrowModel 인터페이스 + escrowClient 구조체)를 만든다.
gogin이 이 산출물을 읽거나 참조하지 않는다 — 완전 독립.
진입점
| 함수 |
파일 |
역할 |
Generate(source, outputDir) |
generate.go:16 |
파일 I/O 래퍼: source 로드 → generate → 쓰기 |
GenerateBytes(source, data) |
generate_bytes.go:12 |
테스트용: 바이트 받아 바이트 반환 |
generateCode(svcName, doc) |
generate_code.go:13 |
코어 생성 로직 |
Generate 시그니처
func Generate(source, outputDir string) error
source — OpenAPI 문서의 파일 경로 또는 http(s):// URL.
outputDir — 산출 .go 파일 출력 경로.
- 산출물 이름 —
{serviceName}.go (lowercase).
readSource
read_source.go:13:
http:// / https:// 접두 시 HTTP GET.
- 아니면 로컬 파일 읽기.
서비스명 추론
inferServiceName infer_service_name.go:12:
- 우선:
OpenAPI.Info.Title 에서 PascalCase 추출.
- fallback: 파일명에서
.openapi.yaml 제거 후 PascalCase.
산출물 형태
// Code generated by fullend gen-model. DO NOT EDIT.
package external
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"time" // date-time 필드 존재 시만
)
type EscrowModel interface {
Hold(ctx context.Context, gigId int64, amount int64) (*HoldResponse, error)
GetStatus(ctx context.Context, id int64) (*GetStatusResponse, error)
Cancel(ctx context.Context, id int64) error
}
type HoldResponse struct {
ID int64 `json:"id"`
Status string `json:"status"`
}
type escrowClient struct {
baseURL string
client *http.Client
}
func NewEscrowModel(baseURL string) EscrowModel {
return &escrowClient{baseURL: baseURL, client: &http.Client{}}
}
func (c *escrowClient) Hold(ctx context.Context, gigId int64, amount int64) (*HoldResponse, error) {
body := map[string]any{"gigId": gigId, "amount": amount}
var resp HoldResponse
if err := c.do(ctx, "POST", "/escrow/hold", body, &resp); err != nil {
return nil, err
}
return &resp, nil
}
func (c *escrowClient) do(ctx context.Context, method, path string, body any, out any) error {
// JSON marshal → http.NewRequest → c.client.Do → JSON unmarshal
}
핵심 파이프라인
generateCode generate_code.go:13-77:
- 메서드 추출 —
extractMethods(doc) → []methodInfo.
- 응답 타입 추출 —
extractResponseTypes(svc, methods, doc) → []structType.
- time 필요 여부 —
typesNeedTime(types) → time.Time 필드 존재 시 import 추가.
- 인터페이스 렌더 —
{ServiceName}Model interface { … }.
- 응답 구조체 렌더 — 각 method의 200 응답을 struct로.
- 클라이언트 구조체 —
{lowerSvc}Client { baseURL, client }.
- 생성자 —
New{ServiceName}Model(baseURL) …Model.
- 메서드 구현 — 각
methodInfo.implementation().
- do() 헬퍼 —
do_helper.go:7.
- gofmt —
format.Source().
OpenAPI 파싱 단계
| 단계 |
함수 |
파일 |
| path 순회 |
extractMethods |
extract_methods.go:9 |
| path 내 메서드 순회 |
extractPathMethods |
extract_path_methods.go:9 |
| methodInfo 조립 |
buildMethodInfo |
build_method_info.go:9 |
| path 파라미터 추출 |
extractPathParams |
extract_path_params.go:9 |
| body 파라미터 추출 |
extractBodyParams |
extract_body_params.go:9 |
| 반환 타입 감지 |
detectReturnType |
detect_return_type.go:9 |
| 응답 구조 추출 |
extractResponseTypes |
extract_response_types.go:9 |
| 오퍼레이션 검색 |
findOperation |
find_operation.go:9 |
methodInfo
method_info_type.go:5
type methodInfo struct {
Name string // Hold, GetStatus
HTTPMethod string // POST, GET
Path string // /escrow/hold, /escrow/{id}
Params []paramInfo // Path + Body 파라미터
ReturnType string // HoldResponse, ""
}
관련 메서드:
signature() method_info_signature.go:10 — 인터페이스 선언 줄
implementation() method_info_implementation.go:11 — 구현 본문
buildPathExpr() method_info_build_path_expr.go:10 — /escrow/%d + fmt.Sprintf
pathParams() / bodyParams() — 필터링
스키마 → Go 타입
schemaToGoType schema_to_go_type.go:9-53
| OpenAPI type |
format |
Go |
| integer |
int64 |
int64 |
| integer |
int32 |
int32 |
| integer |
(없음) |
int |
| number |
float |
float32 |
| number |
double / (없음) |
float64 |
| string |
(없음) |
string |
| string |
date-time |
time.Time |
| boolean |
- |
bool |
| array |
items 있음 |
[]T (재귀) |
| array |
items 없음 |
[]any |
| object |
- |
map[string]any |
| (타입 없음) |
- |
any |
$ref는 kin-openapi가 자동 해석.
time.Time 쓰임 검출: typesNeedTime types_need_time.go:5 + structHasTimeField struct_has_time_field.go:5.
이름 규칙
| 함수 |
예 |
toPascalCase("escrow_service") |
"EscrowService" |
toCamelCase("escrow_service") |
"escrowService" |
lcFirst("EscrowModel") |
"escrowModel" |
모두 strcase 라이브러리 기반 (to_pascal_case.go:7, to_camel_case.go:7, lc_first.go:7).
정렬
결정적 출력을 위해 맵 키 순회는 항상 정렬:
sortedKeys sorted_keys.go:11 — schema property 맵
sortedPathKeys sorted_path_keys.go:11 — OpenAPI paths
테스트
testdata/escrow.openapi.yaml — 4개 작업(hold/release/getStatus/cancel), path param 1개(/escrow/{id}), body param 2개, 200 JSON 3개 + 204 1개.
| 테스트 |
파일 |
검증 |
| 전체 생성 |
test_generate_escrow_test.go:11 |
인터페이스·메서드·응답타입·do헬퍼·context·path 포매팅 |
| 스키마 변환 |
test_schema_to_go_type_test.go:11 |
12 케이스 매핑 테이블 |
| 서비스명 추론 |
test_infer_service_name_test.go:11 |
파일명/Title/URL fallback |
| 파일 쓰기 |
test_generate_write_file_test.go |
경로·파일명 검증 |
파일 맵 (40+)
| 카테고리 |
파일 |
| 공개 API |
generate.go, generate_bytes.go, generate_code.go |
| 소스 로딩 |
read_source.go, infer_service_name.go |
| 추출 |
extract_methods.go, extract_path_methods.go, extract_path_params.go, extract_body_params.go, extract_response_types.go, detect_return_type.go, find_operation.go |
| methodInfo |
method_info_type.go, build_method_info.go, method_info_signature.go, method_info_implementation.go, method_info_build_path_expr.go, method_info_path_params.go, method_info_body_params.go |
| 타입 / 구조 |
param_info.go, struct_type.go, struct_field.go, schema_to_go_type.go, types_need_time.go, struct_has_time_field.go |
| 렌더 헬퍼 |
write_body_map.go, write_return_with_result.go, do_helper.go |
| 이름 변환 |
to_pascal_case.go, to_camel_case.go, lc_first.go |
| 정렬 |
sorted_keys.go, sorted_path_keys.go |
설계 메모
- 결정적 출력 — 모든 맵 순회를 정렬 기반으로 해서 diff가 안정적.
- time 의존성 자동화 —
time.Time 필드 없으면 time import도 안 나옴.
- 최소 표면적 — 공개 함수 2개(
Generate, GenerateBytes)만.
- 외부 서비스 전용 — 내부 DB/Model과 완전 분리. 독립 CLI 하위 명령(
fullend gen-model)으로도 사용 가능.