본문 바로가기

개발

protocol-buffers-schema 로 보는 proto 구조 이해하기(1)

protocol-buffers-schema

.proto 파일을 jsonSchema transpile하는 도구를 작업하려고하면서
protobuf를 규칙에따라 js object형으로 파싱하는 것이 필요해졌다.
protocol-buffers-schema는 protobuf schema를 전환해주는 API를 js JSON객체 처럼 제공하고있어 이를 선택하게되었고, 이를 통해 proto의 구조를 좀더 구체적으로 파악해보고자한다.

var parse = function

parse(fs.readFileSync(filename, 'utf-8'))
우선 parse는 Buffer file을 인자로 받는다.

tokenize

tokenize(buf.toString())
buffer를 tokenize하는 과정을 갖게되는데

sch
  .replace(/"(\\"|[^"\n])*?"|'(\\'|[^'\n])*?'/gm, removeQuotedLines(replacements))
  .replace(/([;,{}()=:[\]<>]|\/\*|\*\/)/g, ' $1 ')
  .split(/\n/)
  .map(trim)
  .filter(Boolean)
  .map(noComments)
  .map(trim)
  .filter(Boolean)
  .join('\n')
  .split(/\s+|\n+/gm)
  .filter(noMultilineComments())
  .map(restoreQuotedLines(replacements))

Quoted 요소에 대해서 replacements 배열에 담아두고 trim, 주석제거, 개행제거 과정을 거친후 Quoted요소를 재조합하여 구문들을 token단위로 나누어 배열을 반환한다.

schema

var schema = {
    syntax: 3,
    package: null,
    imports: [],
    enums: [],
    messages: [],
    options: {},
    extends: []
}

protocol-buffers-schema는 proto3 를 default syntax로 이용하고있다.

proto3가면서의 변경점
https://www.crankuptheamps.com/blog/posts/2017/10/12/protobuf-battle-of-the-syntaxes/#
https://github.com/protocolbuffers/protobuf/releases/tag/v3.0.0

proto의 root에 들어갈 수 있는 기본적인 형태의 문법을 확인할수있다.

read tokens

while을 통해 tokens를 전체 탐색과정을 갖는데 switch문을 통해 구문의 operation을 파악하고 해당 구문의 내용을 tokens.shift()를 통해 인자를 인출해주어 원하는 정보를 schema에 담는다.

 

case 'package'

var onpackagename = function (tokens) {
  tokens.shift()
  var name = tokens.shift()
  if (tokens[0] !== ';') throw new Error('Expected ; but found ' + tokens[0])
  tokens.shift()
  return name
}

 

package는 프로젝트 이름을 기반으로하는 고유한 이름을 가져가며,
가능하면 protocol buffer type 정의를 포함하는 파일 경로를 기반하는게 좋다.

 

case 'syntax'

proto의 버전을 명시.

var onsyntaxversion = function (tokens) {
  ...
  switch (version) {
    case '"proto2"':
      version = 2
      break

    case '"proto3"':
      version = 3
      break

    default:
      throw new Error('Expected protobuf syntax version but found ' + version)
  }
  ...
  return version
}

 

case 'message'

message필드는 하위에 계속하여 root에서와 동일한 field들을 포함하며, message name에 대해서만 추가적인 형태이다.

따라서 onmessagebody를 재귀적으로 호출하며 msg를 읽어나가게 된다.

var onmessage = function (tokens) {
  tokens.shift()

  var lvl = 1
  var body = []
  var msg = {
    name: tokens.shift(),
    options: {},
    enums: [],
    extends: [],
    messages: [],
    fields: []
  }

  if (tokens[0] !== '{') throw new Error('Expected { but found ' + tokens[0])
  tokens.shift()

  while (tokens.length) {
    if (tokens[0] === '{') lvl++
    else if (tokens[0] === '}') lvl--

    if (!lvl) {
      tokens.shift()
      body = onmessagebody(body)
      msg.enums = body.enums
      msg.messages = body.messages
      msg.fields = body.fields
      msg.extends = body.extends
      msg.extensions = body.extensions
      msg.options = body.options
      return msg
    }

    body.push(tokens.shift())
  }

  if (lvl) throw new Error('No closing tag for message')
}

메시지 이름은 CamelCase를 사용하며 필드이름들에는 underscore_separated_names을 사용해야합니다.
필드 이름에 숫자가 포함된 경우에는 숫자는 밑줄을 치지않고 문자 뒤표기합니다.
예를 들어 song_name_1 대신 song_name1을 사용해줍니다.

 

case 'enum'

enum필드의 구분자는 쉼표가 아닌 세미콜론으로 이를 기준으로 확인.

var onenum = function (tokens) {
  tokens.shift()
  var options = {}
  var e = {
    name: tokens.shift(),
    values: {},
    options: {}
  }

  if (tokens[0] !== '{') throw new Error('Expected { but found ' + tokens[0])
  tokens.shift()

  while (tokens.length) {
    if (tokens[0] === '}') {
      tokens.shift()
      // there goes optional semicolon after the enclosing "}"
      if (tokens[0] === ';') tokens.shift()
      return e
    }
    if (tokens[0] === 'option') {
      options = onoption(tokens)
      e.options[options.name] = options.value
      continue
    }
    var val = onenumvalue(tokens)
    if (val !== null) {
      e.values[val.name] = val.val
    }
  }

  throw new Error('No closing tag for enum')
}


enum은 여러 값을 갖는 필드들을 갖기에 onenumvalue를 각 필드내에서 호출하며 각 필드별로 option필드를 지녀 onoption을 호출한다.

enum name은 CamelCase를 사용하며 값에는 CAPITALS_WITH_UNDERSCORES를 활용한다.

  • case 'option'
  • case 'import'
  • case 'extend'
  • case 'service'

1일 1포스트를 지향하기에 남은 타입은 내일 이어서 쓰려고한다.