ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Jsonnet 톺아보기
    programing/Language 2021. 5. 19. 15:00

    Jsonnet 이란?

    A data templating language for app and tool developers

    Jsonnet은 JSON과 sonnet의 합성어로, 데이터 템플레이팅 언어입니다. 기존의 데이터 구조화에 사용되던 언어인 JSON의 확장판이라고 보시면 됩니다.

    JSON에 프로그래밍의 일부 기술을 접목했다고 보시면 될 것 같습니다.

    기본적으로는 C++로 구현되어 있지만, 현재 Google이 관리하고 있어서 그런지 GO로 구현된 go-jsonnet도 있습니다.

    현재 버전은 v.0.17.0이지만, 태그 날짜를 보니 활발히 개발되고 있는 듯 합니다.

    참고로 발음은 "jay sonnet"이라고 합니다.

    철학

    Jsonnet은 각기 다른 환경을 하나로 통합하는 것을 목표로 개발되었습니다. 각각의 구성을 독립적으로 구성하면 중복 및 유지보수가 어려워지기 때문입니다.

    만약 당신이 응용프로그램 개발자라면 애플리케이션이 JSON 혹은 구조화된 포멧을 사용하도록 하면 됩니다. 사용자는 Jsonnet을 이용해 자신만의 설정 파일을 생성하면 됩니다.

    Jsonnet은 풀 프로그래밍 언어이므로, 어떠한 로직도 자유롭게 구현할 수 있습니다. 다만, 제한없는 IO는 허용하지 않으므로, 입출력이 명확하고 한정적인 경우에 사용하는 것이 좋습니다. 구성이라기 보다, 어플리케이션으로 사용되는 몇가지 사례는 다음과 같습니다. (그런데 저는 잘 이해가 안되네요.. 🤔)

    • 정적 사이트 생성
    • 임베딩된 표현 언어
    • Ad hoc JSON transformations (뭔말이야??)
    • 교육

    특징

    • Generate config data : 설정 파일을 생성하는데 용이합니다. (애초에 데이터 템플레이팅 언어니까요..)
    • Side-effect free : 사이드 이펙트가 없습니다. (Jsonnet이 함수형 언어이기 때문입니다.. 만 구현에 따라서 충분히 사이드 이펙트가 있을 수 있습니다.)
    • Organize, simplify, unify : 구조화, 단순화, 통일성
    • Manage sprawling config : 무분별한 설정들을 관리하기 용이합니다.

    Functional Language

    Jsonnet은 OO를 포함한 함수형 언어이기 때문에, 기존의 명령형 언어에 익숙하다면 조금 낯설 수 있습니다. 즉, 타 언어가 서술적이라면 함수형 언어는 선언적(무엇을 할 지)이기 때문입니다. 이러한 특성 덕분에 사용자는 상태와 명령의 순서에 대해 고민할 필요가 없습니다. 😎

    Extension of JSON

    • Open source (Apache 2.0) : 비교적 자유로운 아파치 라이센스 오픈 소스입니다.
    • Familiar syntax : JS 및 phthon과 유사한 문법을 가지고 있습니다.
    • Reformatter, linter : 리포맷 및 린트를 적용가능한 듯 합니다.
    • Editor & IDE integrations : 에디터 지원이 된다고 합니다.
    • Formally specified : 공식적으로 지정된..? 잘 모르겠네요. 😅

    위에 적힌 것들 외에도 Computed Field Name, Python 스타일 배열 및 객체, 객체 지향(상속) 등을 지원합니다. 😮

    WebStorm에는 플러그인이 존재합니다

    문법

    기본

    • 유효한 식별자(아마도 스코프 내에서 유일한 값)인 필드는 따옴표가 필요없습니다.
    • Trailing comma는 필수입니다.
    • 주석을 지원합니다. (/* */# )
    • 스트링 리터럴은 "' 둘 다 지원합니다.
    • python과 유사하게, ||| 를 이용하면 멀티 라인 스트링을 작성할 수 있습니다.
    • 단일 라인은 @'foo' 로 작성할 수 있습니다.

    변수

    • local 키워드를 통해 변수를 선언 및 할당합니다.
    • 필드와 같은 레벨에 선언할 수 있으며, 이 때는 JS처럼 , 로 끝냅니다.
    • 그 외의 경우에는 항상 ; 로 끝냅니다.
    // A regular definition.
    local house_rum = 'Banks Rum';
    
    {
      // A definition next to fields.
      local pour = 1.5,
    
      Daiquiri: {
        ingredients: [
          { kind: house_rum, qty: pour },
          { kind: 'Lime', qty: 1 },
          { kind: 'Simple Syrup', qty: 0.5 },
        ],
        served: 'Straight Up',
      },
    }
    {
      "Daiquiri": {
        "ingredients": [
          {
            "kind": "Banks Rum",
            "qty": 1.5
          },
          {
            "kind": "Lime",
            "qty": 1
          },
          {
            "kind": "Simple Syrup",
            "qty": 0.5
          }
        ],
        "served": "Straight Up"
      }
    }

    house_rumpour 변수를 활용한 모습입니다.

    레퍼런스

    • self 는 현재 객체를 가리킵니다.
    • $ 는 가장 바깥의 객체를 가리킵니다.
    • ['some_filed']some_filed 라는 이름의 필드를 찾습니다.
    • JS의 객체와 같이, . 을 이용해 특정 객체의 필드에 접근할 수 있습니다.
    • 배열과 같이, [] 를 이용해 특정 인덱스 요소를 참조할 수 있으며, python 처럼 슬라이싱이 가능합니다.
    • Arbitrarily long paths are allowed.
    • Array slices like arr[10:20:2] are allowed, like in Python.
    • Strings can be looked up / sliced too, by unicode codepoint.

    연산자

    서로 다른 타입의 값을 연산할 수 있습니다.

    • 부동소수점 산술, 비트 연산, 불 논리를 사용합니다.
    • JS처럼 + 를 이용해 문자열 연산을 할 수 있습니다. (임의의 타입 변경도 포함)
    • 두 문자열은 < 를 이용해 비교할 수 있습니다. (유니코드 순서).
    • + 를 이용해 다른 객체를 상속할 수 있습니다. 단, 충돌나는 필드는 오른쪽 객체로 오버라이딩 합니다.
    • phython처럼, 객체에 필드의 유뮤를 판단할 때 in 을 사용합니다.
    • 깊은 비교를 할 땐 == 를 사용합니다.
    • python의 옛날 문법인 %-포매팅을 지원합니다.
    {
      concat_array: [1, 2, 3] + [4],
      concat_string: '123' + 4,
      equality1: 1 == '1',
      equality2: [{}, { x: 3 - 1 }]
                 == [{}, { x: 2 }],
      ex1: 1 + 2 * 3 / (4 + 5),
      // Bitwise operations first cast to int.
      ex2: self.ex1 | 3,
      // Modulo operator.
      ex3: self.ex1 % 2,
      // Boolean logic
      ex4: (4 > 3) && (1 <= 3) || false,
      // Mixing objects together
      obj: { a: 1, b: 2 } + { b: 3, c: 4 },
      // Test if a field is in an object
      obj_member: 'foo' in { foo: 1 },
      // String formatting
      str1: 'The value of self.ex2 is '
            + self.ex2 + '.',
      str2: 'The value of self.ex2 is %g.'
            % self.ex2,
      str3: 'ex1=%0.2f, ex2=%0.2f'
            % [self.ex1, self.ex2],
      // By passing self, we allow ex1 and ex2 to
      // be extracted internally.
      str4: 'ex1=%(ex1)0.2f, ex2=%(ex2)0.2f'
            % self,
      // Do textual templating of entire files:
      str5: |||
        ex1=%(ex1)0.2f
        ex2=%(ex2)0.2f
      ||| % self,
    }
    {
      "concat_array": [
        1,
        2,
        3,
        4
      ],
      "concat_string": "1234",
      "equality1": false,
      "equality2": true,
      "ex1": 1.6666666666666665,
      "ex2": 3,
      "ex3": 1.6666666666666665,
      "ex4": true,
      "obj": {
        "a": 1,
        "b": 3,
        "c": 4
      },
      "obj_member": true,
      "str1": "The value of self.ex2 is 3.",
      "str2": "The value of self.ex2 is 3.",
      "str3": "ex1=1.67, ex2=3.00",
      "str4": "ex1=1.67, ex2=3.00",
      "str5": "ex1=1.67\nex2=3.00\n"
    }

    함수

    python처럼 위치 파라미터, 이름있는 파라미터, 기본 값을 지원합니다. 또한 JS처럼 클로저를 지원하며, 함수는 일급 객체입니다.

    // A function that returns an object.
    local Person(name='Alice') = {
      name: name,
      welcome: 'Hello ' + name + '!',
    };
    {
      person1: Person(),
      person2: Person('Bob'),
    }
    {
      "person1": {
        "name": "Alice",
        "welcome": "Hello Alice!"
      },
      "person2": {
        "name": "Bob",
        "welcome": "Hello Bob!"
      }
    }

    순수 함수 Person 을 이용해서 특정 로직을 캡슐화 및 재사용 할 수 있습니다.

    포멧 변환

    local application = 'my-app';
    local module = 'uwsgi_module';
    local dir = '/var/www';
    local permission = 644;
    
    {
      'uwsgi.ini': std.manifestIni({
        sections: {
          uwsgi: {
            module: module,
            pythonpath: dir,
            socket: dir + '/uwsgi.sock',
            'chmod-socket': permission,
            callable: application,
            logto: '/var/log/uwsgi/uwsgi.log',
          },
        },
      }),
    
      'init.sh': |||
        #!/bin/bash
        mkdir -p %(dir)s
        touch %(dir)s/initialized
        chmod %(perm)d %(dir)s/initialized
      ||| % {dir: dir, perm: permission},
    
      'cassandra.conf': std.manifestYamlDoc({
        cluster_name: application,
        seed_provider: [
          {
            class_name: 'SimpleSeedProvider',
            parameters: [{ seeds: '127.0.0.1' }],
          },
        ],
      }),
    }
    // cassandra.conf
    "cluster_name": "my-app"
    "seed_provider":
    - "class_name": "SimpleSeedProvider"
      "parameters":
      - "seeds": "127.0.0.1"
    
    // init.sh
    #!/bin/bash
    mkdir -p /var/www
    touch /var/www/initialized
    chmod 644 /var/www/initialized
    
    // uwsgi.ini
    [uwsgi]
    callable = my-app
    chmod-socket = 644
    logto = /var/log/uwsgi/uwsgi.log
    module = uwsgi_module
    pythonpath = /var/www
    socket = /var/www/uwsgi.sock

    JSON을 포함하여 YAML, INI 등 다양한 형식으로의 변환을 지원합니다.

    기타

    Standard Library를 통해 여러가지 부가 기능을 제공하고 있습니다.

    • 외부 변수
    • 리플렉션
    • 연산 유틸 함수
    • 테스트 및 디버깅
    • 문자열 연산
    • 파싱

    설치

    brew install jsonnet
    pip install jsonnet

    혹은 GCC로도 빌드가 가능합니다.

    사용법

    인터프리터

    가장 간단한 방법은, phython처럼 인터프리터를 이용해 jsonnet 파일을 데이터 포메팅 파일로 변환할 수 있습니다.

    jsonnet -e <jsonnet_code>
    jsonnet <file_name> > <output_name>

    활용

    제 경우에는 그렇게 복잡한 설정 파일을 만들 일이 별로 없다 보니, 실용성은 아직까지는 크게 와닿지 않습니다..만, 다음과 같은 경우에 유용하다고 합니다.

    • 기본 설정을 여러 환경이 개별적으로 확장해서 사용할 때
    // base.libsonnet
    {
      api: {
        timeout: 10,
      }
    }
    
    // staging.jsonnet
    import 'base.libsonnet'
    
    // production.jsonnet
    (import 'base.libsonnet') + {
      rack_timeout: 10
    }
    • 외부로부터 설정 값을 받고 싶을 때
    // config.jsonnet
    {
      secret_key: std.extVar("MY_SECRET_KEY")
    }
    // command
    $ jsonnet config.jsonnet --ext-str MY_SECRET_KEY=foobarbaz
    

    찍먹

    // fe_study.jsonnet
    local study_name = 'FE_STUDY';
    local study_participant = ['YS', 'HJ', 'TH', 'TE', null];
    local makeInfo(name) = {
        local privateField = "you can't see me, %s",
        name: name,
        age: 20,
        isPrivate: if name == null then privateField % name else 'hello~',
    };
    
    {
        name : study_name,
            name : 'foo',
        participants: [makeInfo(participant) for participant in study_participant]
    }
    # 문법 에러가 있는 경우
    $ jsonnet fe_study.jsonnet 
    STATIC ERROR: fe_study.jsonnet:10:5-9: duplicate field: name
    # 정상적으로 인터프리팅이 된 경우
    {
       "name": "FE_STUDY",
       "participants": [
          {
             "age": 20,
             "isPrivate": "hello~",
             "name": "YS"
          },
          {
             "age": 20,
             "isPrivate": "hello~",
             "name": "HJ"
          },
          {
             "age": 20,
             "isPrivate": "hello~",
             "name": "TH"
          },
          {
             "age": 20,
             "isPrivate": "hello~",
             "name": "TE"
          },
          {
             "age": 20,
             "isPrivate": "you can't see me, null",
             "name": null
          }
       ]
    }

    참고

    Jsonnet 공식 홈페이지

    Jsonnet 공식 저장소

    Jsonnet 소개

    댓글

Designed by black7375.