Coders CLI
Coders는 하나의 DSL(.jssp)로 다양한 플랫폼의 네이티브 코드를 생성하는 트랜스파일러입니다. 내부적으로 플랫폼별 프롬프트를 사용해 선언된 리소스(예: controller, mapper, message, error)를 1:1로 변환하며, 동일한 DSL을 여러 언어/프레임워크로 확장할 수 있습니다.
빠른 시작은 퀵가이드, 설치는 코더스란?에서 확인하세요.
구성 메모
config.yml은 CLI 동작 전반을 제어합니다.
entry: 기본으로 파싱할 루트.jssp파일입니다.projects: 각 항목에platform,name,outPath,entry,target,language,options를 정의합니다.options에는package,namespace,module,mainClass,languageVersion,version,group,description,plugins,dependencies,onlySource,extra,useHistory등을 플랫폼 요구에 맞춰 추가할 수 있습니다.llmOptions: 공급자(provider), 모델(model), 엔드포인트(url), 인증(apiKey), 시간 제한(timeoutSeconds), 스트리밍(stream)을 설정합니다. 추가로 필요한 필드는 같은 블록에 자유롭게 확장할 수 있습니다.
Coders DSL 개요
Coders DSL은 다음 요소로 구성됩니다.
- 선언부:
domain,table,entity,mapper,struct,class,controller,api,define - 실행부:
func,main,@prompt - 리소스:
message,error,property,css - UI:
html+<template>
각 선언은 플랫폼별 프롬프트에 의해 파일/모듈로 분리되어 생성됩니다. 기본 문법은 Syntax 문서에서 다룹니다.
데이터 모델링
Domain
domain Url string(512);
domain Name string(32);Table
table user {
user_id int64 auto;
name Name;
create_at datetime = now();
key(user_id);
index(create_at);
unique index(name);
}Data
data user(name) {
('user1');
('user2');
}Entity
entity UserVo from @table.user {}
entity UserProfileVo {
var userId int64;
var name Name;
var avatarUrl Url;
}Mapper
mapper UserMapper {
query insertUser(name Name) int32 {
insert user(name)
values(:name);
}
query updateUser(userId int64, name Name) int32 {
update user set name = :name
where user_id = :userId;
}
}DB Procedure / Function
dbproc sample(userId string(10), out name string(100)) {
var _name string(100);
var c cursor = select name into _name from user_id = userId;
c.open();
c.fetch();
name = _name;
c.close();
}
dbfunc countUser() int32 {
var _count int32;
select count(*) into _count from user;
return _count;
}애플리케이션 구조
Struct / Class / Interface
struct SignupRequest {
var email string;
var password string;
}
abstract class BaseSample {
func hello() void;
}
class Sample extends BaseSample {
var name string;
constructor(name string) {
this.name = name;
}
func hello() void {
@console.log("hello");
}
}[alias='console']
interface Console {
static func log(message string) void;
static func error(message string) void;
static func warn(message string) void;
}Enum
define enum Status {
READY 1,
RUNNING 2,
DONE 3
}API & 서비스
Controller
[baseUrl='/api/v1', comment='사용자 컨트롤러']
controller UserController {
[method=post, route='/signup']
func signup(@body req SignupRequest) SignupResponse {
var res = SignupResponse();
var n = @mapper.UserMapper.insertUser(req.email, req.password);
if (n <= 0) {
res.code = AppErrors.failed.code;
res.message = AppErrors.failed.message;
return res;
}
res.code = AppErrors.success.code;
return res;
}
}Api (Client)
[comment='사용자 API']
api UserApi from @controller.UserController {}프론트엔드 타겟 (React/Vue/Svelte) 및 Flutter 위젯
Coders DSL의 html 클래스는 프론트엔드 컴포넌트로 변환됩니다. 동일한 DSL을 사용하되, 플랫폼에 따라 결과물이 React, Vue, Svelte 컴포넌트로 매핑됩니다. Flutter는 widget 클래스로 분리되어 위젯으로 생성됩니다.
공통 개념
ref var는 컴포넌트 상태로 변환@model은 입력 바인딩으로 변환@click은 이벤트 핸들러로 변환define css는 스타일 토큰으로 변환@api는 컨트롤러 기반 API 호출로 변환
예시: 로그인 화면 DSL
define css {
text.primary {
text-gray-800
}
}
html LoginPage {
ref var nickname string = "";
ref var password string = "";
func login() {
var res = @api.login(nickname, password);
@console.log(res.code);
}
// template 블록에 UI를 정의한다.
}React 변환 개념
ref var->useState@model->value+onChange@click->onClickdefine css-> 클래스/스타일 토큰 매핑
Vue/Svelte 변환 개념
ref var-> 반응형 상태@model-> v-model 혹은 bind:value@click-> @click/on:click
Flutter 위젯
Flutter는 html 대신 widget 클래스를 사용합니다.
widget LoginPage {
ref var nickname string = "";
ref var password string = "";
func login() {
var res = @api.login(nickname, password);
@console.log(res.code);
}
// widget layout 블록에 UI를 정의한다.
}Flutter 변환 개념
ref var-> State + setState@model-> TextEditingController/OnChanged@click-> onPressed
플랫폼마다 결과물 구조는 다르지만, html/widget 선언이 각각의 컴포넌트/위젯으로 변환된다는 점은 동일합니다.
리소스 정의
Property
define property app {
baseUrl = "https://api.example.com";
}
define property [profile='dev'] app {
baseUrl = "https://dev.api.example.com";
}Message + Message Class
define message[locale='en'] app {
hello: "Hello"
welcome: "Welcome {name}"
}
message MessageApp from @message.app {
static func setLocale(locale string) void;
static var hello string;
static func welcome(name string) string;
}Error + Error Class
define error[locale='en'] app {
not_found (100) = "Resource not found"
server_error (200) = "Internal server error"
}
error AppErrors from @error.app {}CSS + HTML
define css {
text.primary {
text-gray-800 dark:text-gray-100
}
}
html LoginPage {
ref var nickname string = "";
ref var password string = "";
func login() {
var res = @api.login(nickname, password);
@console.log(res.code);
}
template {
div class="@css.text.primary" {
input @model="nickname";
input @model="password";
button @click="login" { "Login" }
}
}
}@prompt 지시문
@prompt는 DSL에 없는 네이티브 코드 패턴을 설명하는 힌트입니다. LLM은 이 문장을 해석해 해당 플랫폼에 맞는 코드를 생성합니다.
func main() {
@prompt `"Hello, World!" 출력`
}@prompt 변환 예시 (Rust/Go/Java)
func main(args list<string>) {
@prompt `args[0] 자연수 까지 소수를 추출하는 에라토스테네스 체 구현`
}use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let n: usize = args.get(1).unwrap_or(&"0".to_string()).parse().unwrap_or(0);
if n < 2 {
return;
}
let mut is_prime = vec![true; n + 1];
is_prime[0] = false;
if n >= 1 {
is_prime[1] = false;
}
let mut p = 2;
while p * p <= n {
if is_prime[p] {
let mut multiple = p * p;
while multiple <= n {
is_prime[multiple] = false;
multiple += p;
}
}
p += 1;
}
for i in 2..=n {
if is_prime[i] {
println!("{i}");
}
}
}package main
import (
"fmt"
"os"
"strconv"
)
func main() {
n := 0
if len(os.Args) > 1 {
if v, err := strconv.Atoi(os.Args[1]); err == nil {
n = v
}
}
if n < 2 {
return
}
isPrime := make([]bool, n+1)
for i := range isPrime {
isPrime[i] = true
}
isPrime[0] = false
if n >= 1 {
isPrime[1] = false
}
for p := 2; p*p <= n; p++ {
if isPrime[p] {
for m := p * p; m <= n; m += p {
isPrime[m] = false
}
}
}
for i := 2; i <= n; i++ {
if isPrime[i] {
fmt.Println(i)
}
}
}import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
int n = 0;
if (args.length > 0) {
try {
n = Integer.parseInt(args[0]);
} catch (NumberFormatException ignored) {
n = 0;
}
}
if (n < 2) {
return;
}
boolean[] isPrime = new boolean[n + 1];
for (int i = 0; i <= n; i++) {
isPrime[i] = true;
}
isPrime[0] = false;
if (n >= 1) {
isPrime[1] = false;
}
for (int p = 2; p * p <= n; p++) {
if (isPrime[p]) {
for (int m = p * p; m <= n; m += p) {
isPrime[m] = false;
}
}
}
for (int i = 2; i <= n; i++) {
if (isPrime[i]) {
System.out.println(i);
}
}
}
}Bash/PowerShell 프롬프트
shell 키워드는 스크립트 함수로 변환됩니다. Bash/PowerShell 플랫폼은 shell 함수가 실제로 실행되도록 러너(main 또는 호출 코드)를 포함합니다.
Bash 변환 규칙
set -euo pipefail을 스크립트 상단에 포함shell함수는 Bash 함수로 변환shell identifier()의 identifier는lower_snake_case권장
shell download_file(url string, dest string) {
@prompt `curl로 파일 다운로드`
}set -euo pipefail
download_file() {
local url="$1"
local dest="$2"
curl -fsSL "$url" -o "$dest"
}
main() {
download_file "$@"
}
main "$@"PowerShell 변환 규칙
shell함수는 PowerShellfunction으로 변환shell identifier()는 Verb-Noun 형태의 PascalCase 권장- 실행을 위해
main또는 직접 호출 코드 포함
shell download_file(url string, dest string) {
@prompt `Invoke-WebRequest로 파일 다운로드`
}function Download-File {
param (
[Parameter(Mandatory = $true)]
[string]$Url,
[Parameter(Mandatory = $true)]
[string]$Dest
)
Invoke-WebRequest -Uri $Url -OutFile $Dest
}
Download-File -Url $Url -Dest $Dest변환 방식 개요
Coders는 DSL 선언을 리소스 단위로 분리한 뒤, 플랫폼별 템플릿에 맞춰 파일을 생성합니다.
- 선언(예: controller, mapper, html/widget)을 분석
- 리소스 타입에 따라 코드 스켈레톤 생성
- @prompt 지시문을 네이티브 코드로 보강
간단한 엔드투엔드 예제
domain Email string(320);
domain Password string(128);
struct SignupRequest {
var email Email;
var password Password;
}
struct SignupResponse {
var code int32;
var message string;
}
table user {
user_id int64 auto;
email Email unique;
password Password;
key(user_id);
}
mapper UserMapper {
query insertUser(email Email, password Password) int32 {
insert user(email, password)
values (:email, :password);
}
}
define error[locale='en'] app {
success (0) = "OK"
failed (100) = "Failed"
}
error AppErrors from @error.app {}
[baseUrl='/api/v1']
controller UserController {
[method=post, route='/signup']
func signup(@body req SignupRequest) SignupResponse {
var res = SignupResponse();
var n = @mapper.UserMapper.insertUser(req.email, req.password);
if (n <= 0) {
res.code = AppErrors.failed.code;
res.message = AppErrors.failed.message;
return res;
}
res.code = AppErrors.success.code;
res.message = AppErrors.success.message;
return res;
}
}