drdot
drdot

Reputation: 3347

How to flatten message with mutable structure into protobuf?

My proto buf format is this:

package main;

message Test {
  optional string id = 1;
  optional string name = 2;
  optional string age = 3;
}

Then I populate the protobuf files from the input in golang using the following code. str is already parsed.

test = &Test{
  id: proto.String(str[0]),
  name:  proto.String(str[1]),
  age: proto.String(str[2]),
},

One condition I have to handle is that one or more optional fields in the Test structure could be absent randomly and I do not know that in advance. How do I handle that in golang?

To give more context, the real data can look like these in the file:

id=1, name=peter, age=24
id=2, age=30
name=mary, age=31
id=100
name=bob
age=11

Upvotes: 1

Views: 2455

Answers (2)

RayfenWindspear
RayfenWindspear

Reputation: 6274

Looks like you can write the parser for each line of your input something like the following.

NOTE: I didn't make the struct with proto values because as an external package, it can't be imported in the playground.

https://play.golang.org/p/hLZvbiMMlZ

package main

import (
    "errors"
    "fmt"
    "strings"
)

var ins = []string{
    `id=1, name=peter, age=24`,
    `id=2, age=30`,
    `name=mary, age=31`,
    `id=100`,
    `name=bob`,
    `age=11`,
}

var ParseError = errors.New("bad parser input")

type Test struct {
    ID   string
    Name string
    Age  string
}

func (t *Test) String() string {
    return fmt.Sprintf("ID: %s, Name: %s, Age: %s", t.ID, t.Name, t.Age)
}

func main() {
    for _, v := range ins {
        t, err := ParseLine(v)
        if err != nil {
            fmt.Println(err)
        } else {
            fmt.Println(t)
        }
    }
}

func ParseLine(inp string) (*Test, error) {
    splt := strings.Split(inp, ",")
    test := &Test{}
    for _, f := range splt {
        fieldVal := strings.Split(strings.TrimSpace(f), "=")
        switch strings.TrimSpace(fieldVal[0]) {
        case "id":
            test.ID = strings.TrimSpace(fieldVal[1])
        case "name":
            test.Name = strings.TrimSpace(fieldVal[1])
        case "age":
            test.Age = strings.TrimSpace(fieldVal[1])
        default:
            return nil, ParseError
        }
    }
    return test, nil
}

Upvotes: 0

RayfenWindspear
RayfenWindspear

Reputation: 6274

You could use a regular expression to change your input strings into valid JSON, the use the encoding/json package to parse it. This has the advantage of letting the json parser take care of everything for you. Here is the regex for your particular case.

Basically, the regex looks for field=value and replaces with "field" : "value" and wraps it in {} to create valid JSON. The commas are left as is.

https://play.golang.org/p/_EEdTB6sve

package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "log"
    "regexp"
)

var ins = []string{
    `id=1, name=peter, age=24`,
    `id=2, age=30`,
    `name=mary, age=31`,
    `id=100`,
    `name=bob`,
    `age=11`,
}

var ParseError = errors.New("bad parser input")
var Regex *regexp.Regexp

type Test struct {
    ID   string
    Name string
    Age  string
}

func (t *Test) String() string {
    return fmt.Sprintf("ID: %s, Name: %s, Age: %s", t.ID, t.Name, t.Age)
}

func main() {
    var err error
    Regex, err = regexp.Compile(`([^,\s]*)=([^,\s]*)`)
    if err != nil {
        log.Panic(err)
    }
    for _, v := range ins {

        test, err := ParseLine(v)
        if err != nil {
            log.Panic(err)
        }
        fmt.Println(test)
    }
}

func ParseLine(inp string) (*Test, error) {
    JSON := fmt.Sprintf("{%s}", Regex.ReplaceAllString(inp, `"$1" : "$2"`))
    test := &Test{}
    err := json.Unmarshal([]byte(JSON), test)
    if err != nil {
        return nil, err
    }
    return test, nil
}

Here is what I believe to be a minimum working case for what you are after, though I am not familiar enough with protocol buffers to get the strings to print right... or even verify if they are correct. Note that this doesn't run in the playground.

package main

import (
    "errors"
    "fmt"
    "log"
    "regexp"
    "github.com/golang/protobuf/jsonpb"
    _ "github.com/golang/protobuf/proto"
)

var ins = []string{
    `id=1, name=peter, age=24`,
    `id=2, age=30`,
    `name=mary, age=31`,
    `id=100`,
    `name=bob`,
    `age=11`,
}

var ParseError = errors.New("bad parser input")
var Regex *regexp.Regexp

type Test struct {
    ID   *string `protobuf:"bytes,1,opt,name=id,json=id" json:"id,omitempty"`
    Name *string `protobuf:"bytes,2,opt,name=name,json=name" json:"name,omitempty"`
    Age  *string `protobuf:"bytes,3,opt,name=age,json=age" json:"age,omitempty"`
}

func (t *Test) Reset() {
    *t = Test{}
}


func (*Test) ProtoMessage() {}
func (*Test) Descriptor() ([]byte, []int) {return []byte{}, []int{0}}

func (t *Test) String() string {
    return fmt.Sprintf("ID: %v, Name: %v, Age: %v", t.ID, t.Name, t.Age)
}

func main() {
    var err error
    Regex, err = regexp.Compile(`([^,\s]*)=([^,\s]*)`)
    if err != nil {
        log.Panic(err)
    }
    for _, v := range ins {

        test, err := ParseLine(v)
        if err != nil {
            fmt.Println(err)
            log.Panic(err)
        }
        fmt.Println(test)
    }
}

func ParseLine(inp string) (*Test, error) {
    JSON := fmt.Sprintf("{%s}", Regex.ReplaceAllString(inp, `"$1" : "$2"`))
    test := &Test{}
    err := jsonpb.UnmarshalString(JSON, test)
    if err != nil {
        return nil, err
    }
    return test, nil
}

Upvotes: 1

Related Questions