text: var onfield = function (f, result) {
    var prefix = f.repeated ? 'repeated' : f.required ? 'required' : 'optional'
    if (f.type === 'map') prefix = 'map<' + f.map.from + ',' + f.map.to + '>'
    if (f.oneof) prefix = ''
  
    var opts = Object.keys(f.options || {}).map(function (key) {
      return key + ' = ' + f.options[key]
    }).join(',')
  
    if (opts) opts = ' [' + opts + ']'
  
    result.push((prefix ? prefix + ' ' : '') + (f.map === 'map' ? '' : f.type + ' ') + f.name + ' = ' + f.tag + opts + ';')
    return result
  }
  
  var onmessage = function (m, result) {
    result.push('message ' + m.name + ' {')
  
    if (!m.options) m.options = {}
    onoption(m.options, result)
  
    if (!m.enums) m.enums = []
    m.enums.forEach(function (e) {
      result.push(onenum(e, []))
    })
  
    if (!m.messages) m.messages = []
    m.messages.forEach(function (m) {
      result.push(onmessage(m, []))
    })
  
    var oneofs = {}
  
    if (!m.fields) m.fields = []
    m.fields.forEach(function (f) {
      if (f.oneof) {
        if (!oneofs[f.oneof]) oneofs[f.oneof] = []
        oneofs[f.oneof].push(onfield(f, []))
      } else {
        result.push(onfield(f, []))
      }
    })
  
    Object.keys(oneofs).forEach(function (n) {
      oneofs[n].unshift('oneof ' + n + ' {')
      oneofs[n].push('}')
      result.push(oneofs[n])
    })
  
    result.push('}', '')
    return result
  }
  
  var onenum = function (e, result) {
    result.push('enum ' + e.name + ' {')
    if (!e.options) e.options = {}
    var options = onoption(e.options, [])
    if (options.length > 1) {
      result.push(options.slice(0, -1))
    }
    Object.keys(e.values).map(function (v) {
      var val = onenumvalue(e.values[v])
      result.push([v + ' = ' + val + ';'])
    })
    result.push('}', '')
    return result
  }
  
  var onenumvalue = function (v, result) {
    var opts = Object.keys(v.options || {}).map(function (key) {
      return key + ' = ' + v.options[key]
    }).join(',')
  
    if (opts) opts = ' [' + opts + ']'
    var val = v.value + opts
    return val
  }
  
  var onoption = function (o, result) {
    var keys = Object.keys(o)
    keys.forEach(function (option) {
      var v = o[option]
      if (~option.indexOf('.')) option = '(' + option + ')'
  
      var type = typeof v
  
      if (type === 'object') {
        v = onoptionMap(v, [])
        if (v.length) result.push('option ' + option + ' = {', v, '};')
      } else {
        if (type === 'string' && option !== 'optimize_for') v = '"' + v + '"'
        result.push('option ' + option + ' = ' + v + ';')
      }
    })
    if (keys.length > 0) {
      result.push('')
    }
  
    return result
  }
  
  var onoptionMap = function (o, result) {
    var keys = Object.keys(o)
    keys.forEach(function (k) {
      var v = o[k]
  
      var type = typeof v
  
      if (type === 'object') {
        if (Array.isArray(v)) {
          v.forEach(function (v) {
            v = onoptionMap(v, [])
            if (v.length) result.push(k + ' {', v, '}')
          })
        } else {
          v = onoptionMap(v, [])
          if (v.length) result.push(k + ' {', v, '}')
        }
      } else {
        if (type === 'string') v = '"' + v + '"'
        result.push(k + ': ' + v)
      }
    })
  
    return result
  }
  
  var onservices = function (s, result) {
    result.push('service ' + s.name + ' {')
  
    if (!s.options) s.options = {}
    onoption(s.options, result)
    if (!s.methods) s.methods = []
    s.methods.forEach(function (m) {
      result.push(onrpc(m, []))
    })
  
    result.push('}', '')
    return result
  }
  
  var onrpc = function (rpc, result) {
    var def = 'rpc ' + rpc.name + '('
    if (rpc.client_streaming) def += 'stream '
    def += rpc.input_type + ') returns ('
    if (rpc.server_streaming) def += 'stream '
    def += rpc.output_type + ')'
  
    if (!rpc.options) rpc.options = {}
  
    var options = onoption(rpc.options, [])
    if (options.length > 1) {
      result.push(def + ' {', options.slice(0, -1), '}')
    } else {
      result.push(def + ';')
    }
  
    return result
  }
  
  var indent = function (lvl) {
    return function (line) {
      if (Array.isArray(line)) return line.map(indent(lvl + '  ')).join('\n')
      return lvl + line
    }
  }
  
  const stringify = function (schema) {
    var result = []
  
    result.push('syntax = "proto' + schema.syntax + '";', '')
  
    if (schema.package) result.push('package ' + schema.package + ';', '')
  
    if (!schema.options) schema.options = {}
  
    onoption(schema.options, result)
  
    if (!schema.enums) schema.enums = []
    schema.enums.forEach(function (e) {
      onenum(e, result)
    })
  
    if (!schema.messages) schema.messages = []
    schema.messages.forEach(function (m) {
      onmessage(m, result)
    })
  
    if (schema.services) {
      schema.services.forEach(function (s) {
        onservices(s, result)
      })
    }
    return result.map(indent('')).join('\n')
  }
  
  export { stringify };
  