모든 타입은 io.elasticore.base.model.pub.rules 패키지에 있습니다. 규칙 모듈은 elasticore-base 를 compileOnly 로
받습니다(첫 룰셋). 여기에는 팀 규칙에 쓰는 것만 모았습니다. 나머지 공개 메서드는 ElastiCORE 자신의
규칙이 쓰는 것이라 버전 사이에 바뀔 수 있습니다.
import io.elasticore.base.model.pub.rules.*;
import static io.elasticore.base.model.pub.rules.Ctx.ctx;
@Rule 과 CodeRules
CodeRules 는 구현할 메서드가 없는 표식 인터페이스입니다. @Rule 이 붙은 메서드 하나가 규칙 하나입니다. 메서드는
void 이고 접근 제어자는 없어도 됩니다. 클래스에는 인자 없는 생성자가 있어야 합니다.
| 속성 | 기본값 | 뜻 |
|---|
value | (필수) | 규칙 id. -PlistRules 에 나오고 suppress 에 씁니다. 팀 접두사를 붙이세요 (ps-…, acme-…) |
target | "entity" | 기여하는 파일 종류 (대상 파일) |
order | 0 | 같은 단계 안에서 작은 것부터. 같으면 등록 순서 |
phase | HEAD | 파일 단위 규칙이 필드 규칙보다 먼저(HEAD)인지 나중(TAIL)인지. 필드 규칙에는 의미 없음 |
메서드 모양이 불리는 때를 정합니다
| 파라미터 | 불리는 때 | target 예 |
|---|
(EntitySpec e) | 그 엔티티의 파일을 만들 때마다 한 번 | entity, dto, srchdto, repository, control … |
(EntitySpec e, FieldSpec f) | 그 파일의 필드마다 한 번 | entity, dto, srchdto |
(DomainSpec d) | 도메인 단위 파일마다 한 번 | mapper, apiErrors, 내가 만든 도메인 파일 |
(EnumSpec e) | 열거형마다 한 번 | enum |
(PortSpec p) | 외부 연동 포트마다 한 번 | port |
(ProtoServiceSpec p) | proto 포트마다 한 번 | proto |
한 파일 안의 순서는 HEAD 파일 규칙 → 필드 규칙(필드마다) → TAIL 파일 규칙입니다. 팀 규칙은 각 단계에서
ElastiCORE 규칙이 모두 끝난 뒤에 돌기 때문에, 기본 규칙이 붙인 것을 빼거나 고칠 수 있고 order 는 팀 규칙끼리의
순서가 필요할 때만 씁니다.
모델 읽기
EntitySpec - 엔티티, dto.yml 의 DTO, 검색 DTO
| 메서드 | DSL 에서 | 예 (펫스토어) |
|---|
getName() | 엔티티 이름 | "PurchaseOrder" |
domainName(), domainClassName() | 도메인 | "petstore", "Petstore" |
hasMeta("x"), metaValue("x") | meta: 의 @x, @x(값) 의 값 | metaValue("expose") → "20" |
metaValues("x") | 같은 단어가 여러 번일 때 모두 | |
metaText("access") | @access(read=public, update=owner) 처럼 짝으로 된 값의 원문 | |
aggregateRoot() | 루트인가 (멤버·값 객체면 false) | PurchaseOrder → true |
containedIn() | 멤버면 소속 루트 이름 | OrderLine → "PurchaseOrder" |
isSplitForBehavior(e.getName()) | 두 파일로 나뉘는 루트인가 (XxxBase + 개발자 클래스) | |
fields(), allFields(), dtoFields(), pkFields() | 자기 필드 / 상속 포함 / DTO 쪽 / 식별자 | |
events(), eventClassName(ev), eventPackage() | @event(...) 이름들, 생성된 이벤트 클래스 이름·패키지 | [Placed, Shipped, Cancelled] |
commands() → name(), params() | commands: 의 업무 명령 | |
queries() → QuerySpec | repository.yml 의 메서드 (target repository) | |
pkg("dto") | env.yml 네임스페이스 키의 패키지 | |
config("ps.maxPageSize") | 그 도메인 env.yml 의 elasticore.config.ps.maxPageSize, 없으면 null | |
tablePrefix(), apiPath() | config.table.prefix, config.apiPath | "ps_", "/api" |
FieldSpec - 필드 한 줄
| 메서드 | DSL 에서 | 예 |
|---|
name(), capitalName() | 필드 이름, 접근자용 | "email", "Email" |
typeName(), coreItemType() | 선언 타입 / List<X> 의 X | |
required() | ! | |
hasDsl("x"), hasDslAny("x", "y") | @x 가 있나 (대소문자 무시) | hasDsl("pii") |
dslValue("x"), dslValueAny(...) | @x(값) 의 값 | dslValue("search") → "like" |
dslProperty("onetomany", "mappedBy") | @OneToMany(mappedBy="parent") 처럼 이름 붙은 속성 | |
maxLength() | string(100) 또는 @length (없으면 0) | |
label(), description() | -- 설명, @label | |
dbColumnName() | @column 또는 snake_case 이름 | |
isList(), isEntityRef(), isBaseType(), isStringType() | 컬렉션 / 다른 엔티티 참조 / 기본 타입 / 문자열 | |
isMoney(), isDate(), isDateTime() | money, date, datetime | |
DomainSpec - 도메인 하나
| 메서드 | 뜻 |
|---|
getName(), className() | "petstore", "Petstore" |
entities() | 도메인의 모든 엔티티 |
namespace("event") | namespace 키의 패키지 (직접 더한 키 포함), 없으면 null |
config("a.b"), isOn("a.b") | env.yml 설정 / "true" 인가 |
EnumSpec - enum.yml
| 메서드 | 뜻 |
|---|
getName() | "OrderStatus" |
constants() → name(), values() | 값들과 각 값의 열 (["DRF", "초안"]) |
fields() | fields: 의 열 이름 |
PortSpec - port.yml
| 메서드 | 뜻 |
|---|
getName(), protocol(), url() | CreditCheckPort, "http"/"dbms", @url(...) |
operations() → name(), httpMethod(), url(), returnType(), params() | methods: 의 호출들 |
QuerySpec - repository.yml 의 메서드
| 메서드 | 뜻 |
|---|
name() | id: |
sql() | query: 문자열. 메서드 이름으로 만드는 쿼리면 null |
isSelect(), isNative() | 조회인가 / nativeQuery: true 인가 |
returnType(), params() | return:, params: |
코드 쓰기
SourceCode - ctx(spec) 이 돌려주는 지금 만드는 파일
ctx(...) 에는 규칙이 받은 것을 그대로 넣습니다: ctx(e), ctx(d), ctx(enumeration), ctx(port).
| 메서드 | 하는 일 |
|---|
ensureClassAnnotation(anno) | 클래스 어노테이션. 이미 있으면 두 번 붙이지 않음. import 자동 |
removeClassAnnotation(fqcn) | 클래스 어노테이션 빼기 (기본 규칙이 붙인 것도) |
field(f) → FieldCode | 그 필드의 선언 |
removeField(f) | 그 필드를 이 파일에서 뺌 |
method("이름") → ensure(anno) | 이름이 같은 메서드 모두에 어노테이션 (repository, port …). 없는 이름이면 빌드 로그에 알림 |
addMethod(source) | 클래스 끝에 손으로 쓴 메서드를 더함 (어노테이션과 메서드) |
excludeMethod("deleteById", …) | 생성되는 엔드포인트 메서드를 뺌 |
addImport(fqcn) | import 등록 (실제로 쓰는 것만 렌더) |
dontMake() | 이 파일을 만들지 않음 (로그에 SKIP(dontMake)) |
var(k, v), var(k, fmt, args…) | 스켈레톤의 ${k} 를 채움. null 은 무시됨 - 빈 값은 "" |
varLine(k, fmt, args…), varLines(k, list) | ${k} 자리에 줄을 더함 |
syntax() | 이 파일의 언어 (Syntax.JAVA, Syntax.KOTLIN, Syntax.TYPESCRIPT) |
requireSyntaxAnyOf(ruleId, Syntax…) | 다른 언어 생성에 쓰이면 조용히 틀린 파일 대신 그 파일을 실패로 |
artifactName() | 도메인 단위 파일 중 어느 것인지 (ContributedArtifact 의 클래스 이름) |
render(), unfilledSlots() | 테스트에서: 파일 전체 문자열 / 채워지지 않은 ${…} 이름 |
FieldCode - 필드 하나
| 메서드 | 하는 일 |
|---|
ensure(anno) | 같은 어노테이션이 없을 때만 붙임 (보통 이것) |
add(anno) | 무조건 붙임 (반복 가능한 어노테이션) |
merge(anno) | 이미 붙은 같은 어노테이션에 속성만 더함, 없으면 붙임 |
replace(anno) | 같은 어노테이션을 이것으로 바꿈 |
removeAnnotation(fqcn) | 빼기 |
type("String"), rename("x"), initial("0") | 선언 타입 / 이름 / 초기값 |
comment(fmt, …), before(fmt, …), after(fmt, …) | 주석 / 선언 위 줄 / 선언 뒤 멤버 |
AnnoCode - 어노테이션
어노테이션을 문자열로 이어 붙이지 마세요. AnnoCode 가 Java({})와 Kotlin([]) 문법, import, 따옴표를 맡습니다.
| 코드 | 결과 |
|---|
AnnoCode.of("jakarta.persistence.Column") | @Column + import |
AnnoCode.of("org.jmolecules.ddd.annotation.%s", "Identity") | 포맷으로 이름 만들기 |
.val("name", "user_id") | name = "user_id" (문자열 - 따옴표 붙음) |
.val("hidden", true), .val("min", 2) | hidden = true, min = 2 |
.valRaw("access", "JsonProperty.Access.WRITE_ONLY") | 그대로 - access = JsonProperty.Access.WRITE_ONLY |
.valRaw("length", "%d", 200) | 포맷 |
.value("x"), .valueRaw("A.B") | 이 름 없는 속성 - @X("x"), @X(A.B) |
.qualified() | 전체 이름으로 (jakarta.persistence.Entity 와 이름이 겹칠 때) |
AnnoCode.AnnoList.of(...) | indexes = {...} 같은 어노테이션 목록 |
ModelCheck - 검사
| 메서드 | 결과 |
|---|
ModelCheck.warn(msg) | 경고만. 같은 메시지는 한 번 |
ModelCheck.fail(msg) | 생성을 끝까지 한 뒤 모든 실패를 모아 보여 주고 빌드 실패 (elcore3 종료 코드 1) |
ModelCheck.failures(), ModelCheck.reset() | 테스트에서 결과 확인 / 테스트 사이 초기화 |
규칙에서 예외를 던지지 마세요. 그 규칙이 쓰던 파일 하나만 실패로 남아 원인을 찾기 어렵습니다.
새 파일
| 타입 | 하는 일 |
|---|
ArtifactContributor → artifactsFor(EntitySpec e) | 엔티티마다 파일을 선언 |
DomainArtifactContributor → artifactsFor(DomainSpec d) | 도메인마다 파일을 선언 |
ContributedArtifact.of(target, namespaceKey, className) | 새 target 이름(= 스켈레톤 이름), 패키지 키, 클래스 이름 |
.orNamespace("entity") | 그 패키지 키가 없는 프로젝트에서 쓸 키 |
스켈레톤은 규칙 모듈의 src/main/resources/elasticore-template/v3/<target>.tmpl (Java),
elasticore-template/kt/<target>.tmpl (Kotlin). 자세한 것은 새 파일 만들기.
Syntax - 언어 이음새
같은 규칙이 Java 와 Kotlin 을 모두 만듭니다. 어노테이션·import 는 생성기가 언어에 맞게 쓰므로 팀 규칙에는 보통 언어
분기가 필요 없습니다. 코드 조각을 직접 쓸 때(새 파일의 줄, addMethod)만 ctx(e).syntax() 로 나누거나 Syntax 에
묻습니다:
| 호출 | Java | Kotlin |
|---|
s.statement("return x") | return x; | return x |
s.getter("dto", "name") | dto.getName() | dto.name |
s.setter("to", "name", "v") | to.setName(v) | to.name = v |
s.localVar("T", "x", "init") | T x = init; | val x = init |
s.newInstance("X", "") | new X() | X() |
s.nullableType("String") | String | String? |
s.classLiteral("X") | X.class | X::class |
s.escapeName("in") | in | `in` |
TypeScript 는 JVM 개념(classLiteral, sam, overriding …)에 답하지 않고 예외를 던집니다. 팀 규칙은 서버(Java·Kotlin)
생성에만 더해지고 TypeScript 클라이언트에는 들어가지 않습니다.