jxn0.3 · 최신

jxn 읽는 법 -- 0.3

jxn(JSON eXplained Notation)은 JSON 의 형태와 뜻을 정의하는 언어(스키마 언어)다. .jxn 파일에 사람이 읽는 정의를 쓰고, 뜻과 관계를 적고, JSON Schema 로 옮긴다.

이 문서는 .jxn 파일과 jxn doc 이 낸 페이지를 읽는 사람을 위한 것이다 -- 낯선 표기를 표기 사전에서 찾는다. 정확한 규칙은 명세에 있다.

한 줄로: { } 는 object, [ ] 는 배열, | 줄은 갈래와 값 목록, 따옴표는 인스턴스에 나오는 키와 값, 맨 낱말은 타입, // 는 뜻, @ 는 주석, 관계 낱말로 시작하는 줄은 관계.

한 파일 따라 읽기#

주문 하나를 정의한 작은 파일이다. 예는 합성이다.

jxn
jxn 0.3

anchor change = $.history : Change          // change[-1] = 바로 앞 Change
anchor catalog external                     // 가게의 상품 목록 -- 인스턴스 밖

{                                           // 주문 하나
    "id": string,                           // 주문 번호
    "items": [{                             // 담은 상품
        "sku": string,                      // 상품 코드
            refers-to catalog.items
        "price": number                     // 담을 때의 값
    }] min 1,
    "total": number,                        // 합계 금액
        derived-from items[*].price         // 담은 상품 값의 합
    "note"?: string,                        // 주문 메모 -- 없을 수 있다
    "coupon": null | string,                // 쿠폰 번호 -- 늘 있고, 없으면 null
    "method": "card" | "cash",              // 결제 방법
    "payment"?: by method                   // 결제 내용 -- 방법에 따라
        | "card" => { "last4": string }     // 카드 끝 네 자리
        | "cash" => absent                  // 현금이면 없다
    "history": [ Change | Note ]            // 주문 이력
}

def Change {                                // 단계가 바뀐 기록
    "from": State,                          // 바뀌기 전 단계
        equals change[-1].to                // 바로 앞 변경의 도착 단계에서 출발한다
    "to": State                             // 바뀐 단계
}

def Note {                                  // 사람이 남긴 메모
    "text": string                          // 메모 글
}

def State =                                 // 주문의 단계
    | "open"                                // 담는 중
    | "paid"                                // 결제됨
    | "shipped"                             // 보냄

이 정의에 맞는 인스턴스:

json
{
  "id": "A-1",
  "items": [{ "sku": "P-10", "price": 1200 }, { "sku": "P-11", "price": 300 }],
  "total": 1500,
  "coupon": null,
  "method": "card",
  "payment": { "last4": "4242" },
  "history": [{ "from": "open", "to": "paid" }, { "text": "포장 부탁" }]
}

줄마다 읽으면:

  1. jxn 0.3 -- 이 파일이 쓰는 문법의 판. (0.3 은 0.2 에 글 속의 링크와 키 참조를 더했다 -- jxn 0.2 파일도 그대로 읽힌다)
  2. anchor change = $.history : Change -- 앵커. $ 는 인스턴스의 뿌리, $.history 는 뿌리의 history 배열이다. : Change 는 그 배열에서 Change 갈래만 센다는 뜻이다. 그래서 change[-1] 은 "바로 앞의 Change" 다 (메모 Note 는 건너뛴다). -> $ · 앵커 · : Def · [-1]
  3. anchor catalog external -- 인스턴스 밖의 것(가게의 상품 목록)을 가리키는 이름. -> external
  4. { -- 이름 없는 { 가 뿌리다. 인스턴스가 { 로 시작하듯 정의도 { 로 시작한다. -> { }
  5. "id": string, -- 키. 따옴표 안이 인스턴스의 키, 뒤의 맨 낱말이 값의 타입, 줄 끝 // 가 뜻이다. 쉼표는 써도 되고 빠져도 된다. -> 키 · //
  6. "items": [{ ... }] min 1, -- 배열. 원소마다 { "sku", "price" } 인 object 이고, 적어도 하나다. -> [ ] · min
  7. refers-to catalog.items -- 관계. 바로 위의 키 sku 가 주어다: "sku 의 값은 카탈로그의 상품을 가리킨다". -> 관계 · 관계 낱말
  8. derived-from items[*].price -- total 은 모든 원소의 price 에서 계산된다. [*] 는 배열의 모든 원소다. -> [*]
  9. "note"?: string -- ? 는 키가 없을 수 있다. "coupon": null | string 은 키는 늘 있고 값이 null 일 수 있다. 둘은 다르다. -> ? · null |
  10. "method": "card" | "cash" -- 허용 값. 따옴표 글은 인스턴스에 실제로 나오는 값이다. -> 허용 값
  11. "payment"?: by method 와 아래 | 줄 -- 갈래. method 가 "card" 이면 payment 는 { "last4": string }, "cash" 이면 payment 키가 없다(absent). -> by · absent
  12. "history": [ Change | Note ] -- 원소가 Change 이거나 Note 인 배열. 어느 쪽인지는 어느 키가 있나로 갈린다. -> def
  13. equals change[-1].to -- from 은 바로 앞 Change 의 to 와 같다. 이웃 원소를 가리키는 주소는 jxn 만의 것이다. -> [-1]
  14. def State = 와 아래 | 줄 -- 값마다 뜻을 단 값 목록. 한 줄로 쓰면 "open" | "paid" | "shipped" 와 같다. -> 값 목록

표기 사전#

표기 뜻 항목
{ } 닫힌 object -- 적힌 키만 object
[ T ] 배열 배열
"key": T 키와 그 값의 타입 키
? 키가 없을 수 있다 없을 수 있다
null | 값이 null 일 수 있다 null
"a" | "b" 허용 값 허용 값
| "a" // 뜻 값마다 뜻을 단 값 목록 값 목록
by k · => 이웃 키에 따라 모양이 다르다 갈래
absent 그 갈래에서는 키가 없다 absent
{ [k]: T } 키가 열린 object (map) map
...Name 다른 모양의 키를 펼친다 펼치기
expect T 기대하는 모양 (원문) expect
any · unknown 무엇이든 · 아직 모른다 any · unknown
min · max 범위 범위
def Name 이름 붙은 모양 def
// 뜻 · 풀이 뜻과 풀이
// ? 열린 물음 물음
// # 머리 -- 무리 가르기 머리
@name 값 주석 주석
@status 정의의 상태 상태
[글](주소) · https://... 바깥 문서로 가는 링크 링크
글 속의 in.body · event 문서 안의 키 참조 (저절로) 키 참조
equals · derived-from ... 관계 관계 · 낱말
when 관계의 조건 when
.sub 주어 좁히기 주어 좁히기
$ 뿌리 뿌리
. 아래 키 아래 키
[*] 배열의 모든 원소 모든 원소
.* map 의 모든 값 모든 값
[-1] · [+1] 이웃 원소 이웃
[..-n] 앞의 모든 원소 앞의 모든 원소
anchor 주소가 시작하는 이름 앵커
: Def 그 갈래만 센다 갈래만
external 인스턴스 밖 external
home 앵커 없는 주소의 시작 home

뼈대 -- 모양#

{ } object#

적힌 키만 있는 object. 적히지 않은 키가 인스턴스에 있으면 틀린 것이다. 이름 없는 { 는 파일의 뿌리(인스턴스 전체)다. 한 줄로 { "last4": string } 처럼도 쓴다. 명세 2-3 · 2-5

[ T ] 배열#

원소마다 T 인 배열. [ Item ] · [ string ] · [ Change | Note ]. 원소가 여러 줄 object 이면 [{ 로 열고 }] 로 닫는다.

text
"items": [{              // 담은 상품
    "sku": string
}] min 1

명세 2-5

"key": T 키#

인스턴스의 키(따옴표 안)와 그 값의 타입. 키는 늘 따옴표로 쓴다 -- JSON 과 같다. 줄 끝의 // 가 그 키의 뜻이다. 명세 2-4

? 없을 수 있다#

"note"?: string -- 키가 없을 수 있다. 있으면 글이다 (null 은 안 된다). TypeScript 의 ?: 와 같다.

json
{ }                    // 된다
{ "note": "문 앞" }    // 된다
{ "note": null }       // 안 된다 -- null 은 null | string 으로 적는다

명세 2-4

null | null 일 수 있다#

"coupon": null | string -- 키는 늘 있고, 값이 null 일 수 있다. "칸은 늘 있고, 할 일이 없으면 null" 인 기록에서 ? 와 다른 뜻이다. 둘 다면 "coupon"?: null | string. 명세 2-4

"a" | "b" 허용 값#

따옴표 글 · 수 · true · false 는 인스턴스에 실제로 나오는 값이다. | 는 "또는". "method": "card" | "cash" 이면 method 는 둘 가운데 하나다. 맨 낱말(string · Item)은 값의 종류(타입)다. 명세 2-5

| 줄 -- 값 목록#

값마다 뜻을 달고 싶을 때, 타입 자리를 비우고 다음 줄부터 | 값 을 쓴다.

text
"state":                 // 주문의 단계
    | "open"             // 담는 중
    | "paid"             // 결제됨

한 줄로 쓴 "state": "open" | "paid" 와 같은 뜻이다. // # 머리로 값을 무리로 가르기도 한다. 명세 2-6

by 갈래#

"payment"?: by method -- payment 의 모양은 이웃 키 method 의 값에 따라 다르다. 아래 | 줄이 갈래다: | "card" => 모양 은 "method 가 "card" 이면 이 모양". 갈래는 method 의 값을 빠짐없이 덮는다.

text
"payment"?: by method
    | "card" => { "last4": string }
    | "cash" => absent

object 안에서 by kind 만 한 줄로 쓰면, kind 의 값에 따라 그 object 의 키 집합이 달라진다. 갈래 안에 적은 관계에는 "그 갈래일 때" 라는 조건이 저절로 붙는다. 명세 2-8

absent#

그 갈래에서는 키가 없다. | "cash" => absent 이면 method 가 "cash" 인 인스턴스에는 payment 키가 없어야 한다. 그래서 그 키는 ? 가 붙는다. 명세 2-8

{ [k]: T } map#

키가 정해지지 않은 object. "stock": { [sku]: integer } 는 "어떤 키든 -- 그 키는 상품 코드(sku) -- 값은 integer".

json
{ "stock": { "P-10": 3, "P-11": 0 } }

명세 2-5

...Name 펼치기#

...Address -- Address 의 키를 이 object 에 그대로 펼친다. 그 위에 키를 더할 수 있다. 명세 2-7

expect T 기대하는 모양#

밖에서 온 원문을 그대로 남기는 칸. 기대하는 모양은 T 지만, 어긋난 원문도 올바른 기록이다 -- 표준 검사는 통과시키고, jxn check 가 어긋남을 알린다. 명세 2-10

any · unknown#

any 는 일부러 연 칸(무엇이든). unknown 은 타입을 아직 모른다 -- jxn doc 페이지에는 "?" 로 보인다. 명세 2-5

min · max 범위#

수에 붙으면 크기(integer min 1), 배열 · 글 · map 에 붙으면 길이([ Item ] min 1 = 적어도 하나). 명세 2-5

def 이름 붙은 모양#

def Item { ... } -- 여러 자리가 같이 쓰는 모양에 이름을 붙인다. 이름은 대문자로 시작한다 (string 같은 기본 타입과 갈린다). def State = "open" | "paid" 처럼 타입식에도 이름을 붙인다. [ Change | Note ] 처럼 object def 둘의 합이면 어느 키가 있나로 갈린다. 기본 타입은 string · integer · number · boolean. 명세 2-3

뜻과 주석#

// 뜻과 풀이#

줄 끝의 // 글 은 그 줄의 뜻(한 문장)이다. 줄 전체가 // 글 이면 바로 위 선언의 풀이(더 할 말)다. 뜻 안의 -- 는 그냥 글이다. 명세 1

// ? 물음#

아직 정하지 못한 것. jxn doc 페이지의 점검 칸에 모인다. 명세 1

// # 머리#

아래 형제들(키 · 값 · 갈래)을 한 무리로 묶는다. 맨 위 수준이면 파일의 절이다. 명세 1

@name 값 주석#

@owner customer @status stable -- 파일이 스스로 선언한 칸(누가 채우나 · 누가 읽나 · 출처 ...). 이름과 값의 꼴은 파일 위쪽의 tag 줄과 list 줄에 있다.

text
list owner                  // 누가 채우나 -- 파일이 선언한 어휘
    | customer              // 고객이 넣은 값
    | system                // 시스템이 계산한 값
tag owner : set<owner>  inherit need

inherit 인 주석은 조상에서 이어받는다 -- jxn doc 의 주석 표에 이어받음 으로 보인다. need 이면 모든 키에 있어야 한다. 이어받기의 순서는 명세 2-14. 명세 2-1 · 2-2

@status 상태#

정의 자체의 상태. jxn 이 준다: stable 정함 · draft 초안 · stub 모름(이름만 있다). jxn doc 의 배지와 점검 칸이 이것을 본다. 명세 2-15

뜻 · 풀이 · 물음 · 값의 뜻 · @src 같은 글 주석 값 안의 [글](주소) 와 맨 https://... 주소는 링크다. jxn doc 페이지에서 누르면 그리로 간다.

text
"total": number,                    // 합계 금액 -- [계산 규칙](docs/계산.md)
    @src [주문 규격 4절](docs/주문.md), https://example.com/order-spec

상대 주소(docs/계산.md)는 그 .jxn 파일이 있는 폴더에서 찾는다. 파일은 열리기만 한다 -- 그 파일의 몇째 줄까지 가지는 않는다. 명세 2-16

키 참조 -- 저절로#

글 안에 적힌 키 이름 · 주소(in.body · items[*].price · event)가 그 자리에서 정의의 키로 풀리면, jxn doc 페이지에서 누를 수 있다 -- 누르면 그 키의 설명이 열린다. 쓰는 사람이 따로 표시하지 않는다. 풀리지 않는 말은 그냥 글이다.

text
"total": number,                    // 합계 금액
    // items[*].price 를 더하고, 할인(discount)이 정액이면 discount.amount 를 뺀다

위에서 items[*].price · discount · discount.amount 가 누를 수 있다. 낱말 하나는 가까운 키(아래 키 · 형제 키 · home)에서 찾는다. 명세 2-16

관계#

관계 줄#

키 아래에 들여 쓴 낱말 주소 줄. 바로 위의 키가 주어다.

text
"total": number,                        // 합계 금액
    derived-from items[*].price         // 담은 상품 값의 합

"total 은 items 의 모든 price 에서 계산된다". 관계는 주어 쪽에 한 번만 적는다 -- jxn doc 페이지는 과녁(price) 쪽에도 같은 낱말로 보여 준다 (거꾸로 절의 derived-from total → 이 키). 관계 낱말은 페이지에서도 파일에 적힌 그대로이고, 아래 표의 뜻과 거꾸로 읽기는 낱말에 마우스를 올리면 뜬다. 명세 2-11

관계 낱말#

낱말 뜻 (주어 쪽) 거꾸로 (과녁 쪽)
equals 과녁과 같다 같다
member-of 과녁(배열)의 원소 · 부분이다 담는다
refers-to 값이 과녁의 식별자다 가리켜진다
follows 과녁 바로 뒤에 붙는다 앞에 선다
copies 과녁의 값을 옮겨 온다 옮겨 간다
derived-from 과녁을 재료로 계산된다 재료다
sets · clears 과녁 상태를 켠다 · 끈다 켜진다 · 꺼진다
adds-to · removes-from 과녁 모음에 넣는다 · 뺀다 넣어진다 · 빠진다
reason-of 과녁의 까닭이다 까닭을 가진다

명세 2-11

when 조건#

derived-from discount.amount when discount.kind == "fixed" -- 그 조건일 때만 서는 관계. == · != · in [...] 과 and. 갈래 안의 관계에는 갈래의 조건이 저절로 붙어, jxn doc 은 합친 조건을 보인다. 명세 2-13

.sub 주어 좁히기#

.code refers-to catalog.vouchers -- 주어를 이 키 아래의 code 로 좁힌다. 여러 자리가 같이 쓰는 모양(def)을 쓰는 자리에서, 그 안쪽 키의 관계를 적을 때 쓴다. 명세 2-11

주소#

관계 · when · anchor 가 가리키는 자리. 뿌리 $ · 모든 원소 [*] · 모든 값 .* 는 JSONPath 와 같다. 명세 2-12

$ 뿌리#

인스턴스 JSON 전체. $.history 는 뿌리의 history 키, $.orders[*] 는 뿌리의 orders 배열의 모든 원소다. jxn doc 페이지의 키 경로도 $ 에서 시작한다 ($.orders[*].total).

. 아래 키#

a.b -- a 안의 키 b. discount.kind 는 discount 안의 kind.

[*] 모든 원소#

배열의 모든 원소. items[*].price 는 "items 의 모든 원소의 price". 앵커에 붙으면 "아무 원소" (order[*].id = 어느 주문이든의 id).

.* 모든 값#

map 의 모든 값. stock.* 는 stock 의 모든 값(재고 수).

[-1] · [+1] 이웃 원소#

앵커 배열에서 지금 원소의 바로 앞([-1]) · 뒤([+1]) 원소. change[-1].to 는 "바로 앞 Change 의 to". JSONPath 의 [-1](마지막 원소)과 다르다 -- jxn 만의 표기다. [-2] 는 두 번째 앞.

[..-n] 앞의 모든 원소#

지금 원소보다 n 번째 앞까지의 모든 원소. change[..-1] 은 지금보다 앞의 모든 Change.

anchor 앵커#

anchor change = $.history : Change -- 주소가 시작하는 이름을 짓는다. 배열에 이름을 지으면 그 이름에 [-1] · [+1] · [*] · [..-n] 을 붙인다. 주소의 첫 이름이 앵커 이름이면 앵커다.

: Def 갈래만 세기#

anchor change = $.history : Change -- history 의 원소 가운데 Change 만 센다. 그래서 change[-1] 은 사이에 낀 다른 갈래(Note)를 건너뛴 바로 앞 Change 다.

external 인스턴스 밖#

anchor catalog external -- 인스턴스에 없는 것(참조 데이터 · 계산으로만 있는 상태). 그리로 가는 관계는 그것을 함께 받아야 검사된다. jxn doc 페이지에는 "밖" 으로 보인다.

home#

home $.history[*] -- 앵커 없이 쓴 주소(in.from 같은)가 시작하는 자리. 없으면 뿌리다. 이력의 한 원소 안에서 이웃 키를 가리킬 때 쓴다.

jxn doc 페이지 읽기#

jxn doc FILE.jxn --example EX.json 이 낸 html 한 파일. 인터넷 없이 열린다.