Skip to content

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

js
domain Url string(512);
domain Name string(32);

Table

js
table user {
  user_id int64 auto;
  name Name;
  create_at datetime = now();
  key(user_id);
  index(create_at);
  unique index(name);
}

Data

js
data user(name) {
  ('user1');
  ('user2');
}

Entity

js
entity UserVo from @table.user {}

entity UserProfileVo {
  var userId int64;
  var name Name;
  var avatarUrl Url;
}

Mapper

js
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

js
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

js
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");
  }
}
js
[alias='console']
interface Console {
  static func log(message string) void;
  static func error(message string) void;
  static func warn(message string) void;
}

Enum

js
define enum Status {
  READY 1,
  RUNNING 2,
  DONE 3
}

API & 서비스

Controller

js
[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)

js
[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

js
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 -> onClick
  • define css -> 클래스/스타일 토큰 매핑

Vue/Svelte 변환 개념

  • ref var -> 반응형 상태
  • @model -> v-model 혹은 bind:value
  • @click -> @click/on:click

Flutter 위젯

Flutter는 html 대신 widget 클래스를 사용합니다.

js
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

js
define property app {
  baseUrl = "https://api.example.com";
}

define property [profile='dev'] app {
  baseUrl = "https://dev.api.example.com";
}

Message + Message Class

js
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

js
define error[locale='en'] app {
  not_found (100) = "Resource not found"
  server_error (200) = "Internal server error"
}

error AppErrors from @error.app {}

CSS + HTML

js
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은 이 문장을 해석해 해당 플랫폼에 맞는 코드를 생성합니다.

js
func main() {
  @prompt `"Hello, World!" 출력`
}

@prompt 변환 예시 (Rust/Go/Java)

js
func main(args list<string>) {
  @prompt `args[0] 자연수 까지 소수를 추출하는 에라토스테네스 체 구현`
}
rust
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}");
        }
    }
}
go
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)
		}
	}
}
java
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 권장
dsl
shell download_file(url string, dest string) {
  @prompt `curl로 파일 다운로드`
}
bash
set -euo pipefail

download_file() {
  local url="$1"
  local dest="$2"
  curl -fsSL "$url" -o "$dest"
}

main() {
  download_file "$@"
}

main "$@"

PowerShell 변환 규칙

  • shell 함수는 PowerShell function으로 변환
  • shell identifier()는 Verb-Noun 형태의 PascalCase 권장
  • 실행을 위해 main 또는 직접 호출 코드 포함
dsl
shell download_file(url string, dest string) {
  @prompt `Invoke-WebRequest로 파일 다운로드`
}
powershell
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 지시문을 네이티브 코드로 보강

간단한 엔드투엔드 예제

js
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;
  }
}