├── codecov.yml ├── .gitignore ├── version.go ├── go.mod ├── Makefile ├── .github └── workflows │ ├── reviewdog.yml │ └── test.yaml ├── CHANGELOG.md ├── go.sum ├── LICENSE ├── example_test.go ├── README.md ├── decode_test.go ├── encode_test.go ├── decode.go └── encode.go /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.gitignore 3 | !.github 4 | dist/ 5 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package ltsv 2 | 3 | const version = "0.1.0" 4 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Songmu/go-ltsv 2 | 3 | go 1.18 4 | 5 | require github.com/kr/pretty v0.3.0 6 | 7 | require ( 8 | github.com/kr/text v0.2.0 // indirect 9 | github.com/rogpeppe/go-internal v1.6.1 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | u := $(if $(update),-u) 2 | 3 | .PHONY: deps 4 | deps: 5 | go get ${u} -d 6 | go mod tidy 7 | 8 | .PHONY: devel-deps 9 | devel-deps: 10 | go install github.com/Songmu/godzil/cmd/godzil@latest 11 | 12 | .PHONY: test 13 | test: 14 | go test 15 | 16 | .PHONY: release 17 | release: devel-deps 18 | godzil release 19 | -------------------------------------------------------------------------------- /.github/workflows/reviewdog.yml: -------------------------------------------------------------------------------- 1 | name: reviewdog 2 | on: [pull_request] 3 | jobs: 4 | staticcheck: 5 | name: staticcheck 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - uses: reviewdog/action-staticcheck@v1 10 | with: 11 | github_token: ${{ secrets.github_token }} 12 | reporter: github-pr-review 13 | fail_on_error: true 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v0.1.0](https://github.com/Songmu/go-ltsv/compare/69c2279845dd...v0.1.0) (2022-05-22) 4 | 5 | * introduce Go Modules and GitHub Actions [#6](https://github.com/Songmu/go-ltsv/pull/6) ([Songmu](https://github.com/Songmu)) 6 | * Support encoding/decoding bool value [#5](https://github.com/Songmu/go-ltsv/pull/5) ([hottestseason](https://github.com/hottestseason)) 7 | * Reduce allocations by avoiding strings.Split [#4](https://github.com/Songmu/go-ltsv/pull/4) ([itchyny](https://github.com/itchyny)) 8 | * Make encoding 2x faster [#3](https://github.com/Songmu/go-ltsv/pull/3) ([harukasan](https://github.com/harukasan)) 9 | * introduce teble driven tests [#2](https://github.com/Songmu/go-ltsv/pull/2) ([Songmu](https://github.com/Songmu)) 10 | * care Null number [#1](https://github.com/Songmu/go-ltsv/pull/1) ([Songmu](https://github.com/Songmu)) 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - "**" 6 | pull_request: {} 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: 14 | - ubuntu-latest 15 | - macOS-latest 16 | - windows-latest 17 | steps: 18 | - name: setup go 19 | uses: actions/setup-go@v3 20 | with: 21 | go-version: 1.x 22 | - name: Set git to use LF 23 | run: | 24 | git config --global core.autocrlf false 25 | git config --global core.eol lf 26 | if: "matrix.os == 'windows-latest'" 27 | - name: checkout 28 | uses: actions/checkout@v3 29 | - name: test 30 | run: go test -race -coverprofile coverage.out -covermode atomic ./... 31 | - name: Send coverage 32 | uses: codecov/codecov-action@v1 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 3 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 4 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 5 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 6 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 7 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 8 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 9 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 10 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 11 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Songmu 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package ltsv_test 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | "github.com/Songmu/go-ltsv" 8 | "github.com/kr/pretty" 9 | ) 10 | 11 | type log struct { 12 | Time *logTime 13 | Host net.IP 14 | Req string 15 | Status int 16 | Size int 17 | UA string 18 | ReqTime float64 19 | AppTime *float64 20 | VHost string 21 | } 22 | 23 | const timeFormat = "2006-01-02T15:04:05Z07:00" 24 | 25 | type logTime struct { 26 | time.Time 27 | } 28 | 29 | func (lt *logTime) UnmarshalText(t []byte) error { 30 | ti, err := time.ParseInLocation(timeFormat, string(t), time.UTC) 31 | if err != nil { 32 | return err 33 | } 34 | lt.Time = ti 35 | return nil 36 | } 37 | 38 | func ExampleUnmarshal() { 39 | ltsvLog := "time:2016-07-13T00:00:04+09:00\t" + 40 | "host:192.0.2.1\t" + 41 | "req:POST /api/v0/tsdb HTTP/1.1\t" + 42 | "status:200\t" + 43 | "size:36\t" + 44 | "ua:ua:mackerel-agent/0.31.2 (Revision 775fad2)\t" + 45 | "reqtime:0.087\t" + 46 | "vhost:mackerel.io" 47 | l := log{} 48 | ltsv.Unmarshal([]byte(ltsvLog), &l) 49 | pretty.Println(l) 50 | // Output: 51 | // ltsv_test.log{ 52 | // Time: time.Date(2016, time.July, 13, 0, 0, 4, 0, time.Location("")), 53 | // Host: {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, 0xff, 0xc0, 0x0, 0x2, 0x1}, 54 | // Req: "POST /api/v0/tsdb HTTP/1.1", 55 | // Status: 200, 56 | // Size: 36, 57 | // UA: "ua:mackerel-agent/0.31.2 (Revision 775fad2)", 58 | // ReqTime: 0.087, 59 | // AppTime: (*float64)(nil), 60 | // VHost: "mackerel.io", 61 | // } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-ltsv 2 | ======= 3 | 4 | [![Test Status](https://github.com/Songmu/go-ltsv/workflows/test/badge.svg?branch=main)][actions] 5 | [![Coverage Status](https://codecov.io/gh/Songmu/go-ltsv/branch/main/graph/badge.svg)][codecov] 6 | [![MIT License](https://img.shields.io/github/license/Songmu/go-ltsv)][license] 7 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/Songmu/go-ltsv)][PkgGoDev] 8 | 9 | [actions]: https://github.com/Songmu/go-ltsv/actions?workflow=test 10 | [codecov]: https://codecov.io/gh/Songmu/go-ltsv 11 | [license]: https://github.com/Songmu/go-ltsv/blob/main/LICENSE 12 | [PkgGoDev]: https://pkg.go.dev/github.com/Songmu/go-ltsv 13 | 14 | LTSV library to map ltsv to struct. 15 | 16 | ## Synopsis 17 | 18 | ```go 19 | import ( 20 | "net" 21 | 22 | "github.com/Songmu/go-ltsv" 23 | ) 24 | 25 | type log struct { 26 | Host net.IP 27 | Req string 28 | Status int 29 | Size int 30 | UA string 31 | ReqTime float64 32 | AppTime *float64 33 | VHost string 34 | } 35 | 36 | func main() { 37 | ltsvLog := "time:2016-07-13T00:00:04+09:00\t" + 38 | "host:192.0.2.1\t" + 39 | "req:POST /api/v0/tsdb HTTP/1.1\t" + 40 | "status:200\t" + 41 | "size:36\t" + 42 | "ua:ua:mackerel-agent/0.31.2 (Revision 775fad2)\t" + 43 | "reqtime:0.087\t" + 44 | "vhost:mackerel.io" 45 | l := &log{} 46 | ltsv.Unmarshal([]byte(ltsvLog), l) 47 | ... 48 | } 49 | ``` 50 | 51 | ## Description 52 | 53 | LTSV parser and encoder for Go with reflection 54 | 55 | ## Installation 56 | 57 | ```console 58 | % go get github.com/Songmu/go-ltsv 59 | ``` 60 | 61 | ## Author 62 | 63 | [Songmu](https://github.com/Songmu) 64 | -------------------------------------------------------------------------------- /decode_test.go: -------------------------------------------------------------------------------- 1 | package ltsv 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestData2Map(t *testing.T) { 9 | l, _ := data2map([]byte("hoge: fuga\tpiyo: piyo")) 10 | 11 | expect := ltsvMap{ 12 | "hoge": "fuga", 13 | "piyo": "piyo", 14 | } 15 | if !reflect.DeepEqual(l, expect) { 16 | t.Errorf("result of data2map not expected: %#v", l) 17 | } 18 | 19 | _, err := data2map([]byte("hoge")) 20 | if err.Error() != "not a ltsv: hoge" { 21 | t.Errorf("something went wrong") 22 | } 23 | } 24 | 25 | type ss struct { 26 | User string `ltsv:"user"` 27 | Age uint8 `ltsv:"age"` 28 | Height *float64 `ltsv:"height"` 29 | Weight float32 30 | EmailVerified bool `ltsv:"email_verified"` 31 | Memo string `ltsv:"-"` 32 | } 33 | 34 | func pfloat64(f float64) *float64 { 35 | return &f 36 | } 37 | 38 | var decodeTests = []struct { 39 | Name string 40 | Input string 41 | Output *ss 42 | }{ 43 | { 44 | Name: "Simple", 45 | Input: "user:songmu\tage:36\theight:169.1\tweight:66.6\temail_verified:true", 46 | Output: &ss{ 47 | User: "songmu", 48 | Age: 36, 49 | Height: pfloat64(169.1), 50 | Weight: 66.6, 51 | EmailVerified: true, 52 | }, 53 | }, 54 | { 55 | Name: "Default values", 56 | Input: "user:songmu\tage:36", 57 | Output: &ss{ 58 | User: "songmu", 59 | Age: 36, 60 | Height: nil, 61 | Weight: 0.0, 62 | EmailVerified: false, 63 | }, 64 | }, 65 | { 66 | Name: "Hyphen and empty string as null number", 67 | Input: "user:songmu\tage:\theight:-", 68 | Output: &ss{ 69 | User: "songmu", 70 | Age: 0, 71 | Height: nil, 72 | Weight: 0.0, 73 | EmailVerified: false, 74 | }, 75 | }, 76 | } 77 | 78 | func TestUnmarshal(t *testing.T) { 79 | m := make(map[string]string) 80 | Unmarshal([]byte("hoge: fuga\tpiyo: piyo"), &m) 81 | expect := map[string]string{ 82 | "hoge": "fuga", 83 | "piyo": "piyo", 84 | } 85 | if !reflect.DeepEqual(m, expect) { 86 | t.Errorf("unmarsharl error:\n out: %+v\n want: %+v", m, expect) 87 | } 88 | 89 | for _, tt := range decodeTests { 90 | t.Logf("testing: %s\n", tt.Name) 91 | s := &ss{} 92 | 93 | err := Unmarshal([]byte(tt.Input), s) 94 | if err != nil { 95 | t.Errorf("%s(err): error should be nil but: %+v", tt.Name, err) 96 | } 97 | 98 | if !reflect.DeepEqual(s, tt.Output) { 99 | t.Errorf("%s:\n out: %+v\n want: %+v", tt.Name, s, tt.Output) 100 | } 101 | } 102 | } 103 | 104 | func BenchmarkUnmarshalStruct(b *testing.B) { 105 | for i := 0; i < b.N; i++ { 106 | for _, tt := range decodeTests { 107 | s := &ss{} 108 | err := Unmarshal([]byte(tt.Input), s) 109 | if err != nil { 110 | b.Error(err) 111 | } 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /encode_test.go: -------------------------------------------------------------------------------- 1 | package ltsv 2 | 3 | import ( 4 | "encoding" 5 | "errors" 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | type ms struct { 11 | err error 12 | } 13 | 14 | func (s *ms) MarshalText() ([]byte, error) { 15 | return []byte("ok"), s.err 16 | } 17 | 18 | var _ encoding.TextMarshaler = &ms{} 19 | 20 | var encodeTests = []struct { 21 | Name string 22 | Input interface{} 23 | Output string 24 | Check func(s string) error 25 | }{ 26 | { 27 | Name: "map", 28 | Input: map[string]string{ 29 | "hoge": "fuga", 30 | "piyo": "piyo", 31 | }, 32 | Check: func(s string) error { 33 | expect1 := "hoge:fuga\tpiyo:piyo" 34 | expect2 := "piyo:piyo\thoge:fuga" 35 | if s != expect1 && s != expect2 { 36 | return fmt.Errorf("result is not expected: %s", s) 37 | } 38 | return nil 39 | }, 40 | }, 41 | { 42 | Name: "Simple with nil pointer", 43 | Input: &ss{ 44 | User: "songmu", 45 | Age: 36, 46 | Height: nil, 47 | Weight: 66.6, 48 | EmailVerified: true, 49 | Memo: "songmu.jp", 50 | }, 51 | Output: "user:songmu\tage:36\tweight:66.6\temail_verified:true", 52 | }, 53 | { 54 | Name: "Simple without nil pointer", 55 | Input: &ss{ 56 | User: "songmu", 57 | Age: 36, 58 | Height: pfloat64(169.1), 59 | Weight: 66.6, 60 | EmailVerified: false, 61 | Memo: "songmu.jp", 62 | }, 63 | Output: "user:songmu\tage:36\theight:169.1\tweight:66.6\temail_verified:false", 64 | }, 65 | { 66 | Name: "Omit memo", 67 | Input: &ss{ 68 | User: "songmu", 69 | Age: 36, 70 | Memo: "songmu.jp", 71 | }, 72 | Output: "user:songmu\tage:36\tweight:0\temail_verified:false", 73 | }, 74 | { 75 | Name: "Anthoer struct", 76 | Input: &struct { 77 | Name string 78 | Value int `ltsv:"answer"` 79 | }{ 80 | Name: "the Answer", 81 | Value: 42, 82 | }, 83 | Output: "name:the Answer\tanswer:42", 84 | }, 85 | { 86 | Name: "TextMarshaler", 87 | Input: &struct { 88 | Struct interface{} 89 | }{ 90 | Struct: &ms{}, 91 | }, 92 | Output: "struct:ok", 93 | }, 94 | } 95 | 96 | func TestMarshal(t *testing.T) { 97 | for _, tt := range encodeTests { 98 | t.Logf("testing: %s\n", tt.Name) 99 | buf, err := Marshal(tt.Input) 100 | if err != nil { 101 | t.Errorf("%s(err): error should be nil but: %+v", tt.Name, err) 102 | } 103 | s := string(buf) 104 | if tt.Check != nil { 105 | err := tt.Check(s) 106 | if err != nil { 107 | t.Errorf("%s: %s", tt.Name, err) 108 | } 109 | } else { 110 | if s != tt.Output { 111 | t.Errorf("%s:\n out =%s\n want=%s", tt.Name, s, tt.Output) 112 | } 113 | } 114 | } 115 | } 116 | 117 | func TestMarshalError(t *testing.T) { 118 | errOK := errors.New("ok") 119 | s := struct { 120 | A *ms 121 | B *ms 122 | }{ 123 | A: &ms{errOK}, 124 | B: &ms{errOK}, 125 | } 126 | 127 | _, err := Marshal(s) 128 | if err == nil { 129 | t.Errorf("got no error") 130 | } 131 | if got := err.(MarshalError).OfField("a"); got != errOK { 132 | t.Errorf("got error: %v", got) 133 | } 134 | if got := err.(MarshalError).OfField("b"); got != errOK { 135 | t.Errorf("got error: %v", got) 136 | } 137 | } 138 | 139 | func BenchmarkMarshalStruct(b *testing.B) { 140 | input := &ss{ 141 | User: "songmu", 142 | Age: 36, 143 | Height: pfloat64(169.1), 144 | Weight: 66.6, 145 | Memo: "songmu.jp", 146 | } 147 | for i := 0; i < b.N; i++ { 148 | _, err := Marshal(input) 149 | if err != nil { 150 | b.Error(err) 151 | } 152 | } 153 | } 154 | 155 | func BenchmarkMarshalMap(b *testing.B) { 156 | input := map[string]string{ 157 | "hoge": "fuga", 158 | "piyo": "piyo", 159 | } 160 | for i := 0; i < b.N; i++ { 161 | _, err := Marshal(input) 162 | if err != nil { 163 | b.Error(err) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /decode.go: -------------------------------------------------------------------------------- 1 | package ltsv 2 | 3 | import ( 4 | "encoding" 5 | "fmt" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | // UnmarshalError is an error type for Unmarshal() 12 | type UnmarshalError map[string]error 13 | 14 | func (m UnmarshalError) Error() string { 15 | if len(m) == 0 { 16 | return "(no error)" 17 | } 18 | 19 | ee := make([]string, 0, len(m)) 20 | for name, err := range m { 21 | ee = append(ee, fmt.Sprintf("field %q: %s", name, err)) 22 | } 23 | 24 | return strings.Join(ee, "\n") 25 | } 26 | 27 | // OfField returns the error correspoinding to a given field 28 | func (m UnmarshalError) OfField(name string) error { 29 | return m[name] 30 | } 31 | 32 | // An UnmarshalTypeError describes a LTSV value that was 33 | // not appropriate for a value of a specific Go type. 34 | type UnmarshalTypeError struct { 35 | Value string 36 | Type reflect.Type 37 | } 38 | 39 | func (e *UnmarshalTypeError) Error() string { 40 | return "ltsv: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() 41 | } 42 | 43 | type ltsvMap map[string]string 44 | 45 | func data2map(data []byte) (ltsvMap, error) { 46 | d := string(data) 47 | l := ltsvMap{} 48 | var i int 49 | var v string 50 | for i < len(d) { 51 | j := strings.IndexByte(d[i:], '\t') 52 | if j >= 0 { 53 | v = d[i : i+j] 54 | i += j + 1 55 | } else { 56 | v = d[i:] 57 | i = len(d) 58 | } 59 | k := strings.IndexByte(v, ':') 60 | if k < 0 { 61 | return nil, fmt.Errorf("not a ltsv: %s", d) 62 | } 63 | l[strings.TrimSpace(v[:k])] = strings.TrimSpace(v[k+1:]) 64 | } 65 | return l, nil 66 | } 67 | 68 | // Unmarshal parses the LTSV-encoded data and stores the result 69 | // in the value pointed to by v. 70 | func Unmarshal(data []byte, v interface{}) error { 71 | rv := reflect.ValueOf(v) 72 | if rv.Kind() != reflect.Ptr { 73 | return fmt.Errorf("not a pointer: %v", v) 74 | } 75 | 76 | rv = rv.Elem() 77 | if rv.Kind() != reflect.Struct && rv.Kind() != reflect.Map { 78 | return fmt.Errorf("not a pointer to a struct/map: %v", v) 79 | } 80 | 81 | l, err := data2map(data) 82 | if err != nil { 83 | return err 84 | } 85 | if rv.Kind() == reflect.Map { 86 | kt := rv.Type().Key() 87 | vt := rv.Type().Elem() 88 | if kt.Kind() != reflect.String || vt.Kind() != reflect.String { 89 | return fmt.Errorf("not a map[string]string") 90 | } 91 | for k, v := range l { 92 | kv := reflect.ValueOf(k).Convert(kt) 93 | vv := reflect.ValueOf(v).Convert(vt) 94 | rv.SetMapIndex(kv, vv) 95 | } 96 | return nil 97 | } 98 | 99 | t := rv.Type() 100 | errs := UnmarshalError{} 101 | for i := 0; i < t.NumField(); i++ { 102 | ft := t.Field(i) 103 | fv := rv.Field(i) 104 | 105 | key := ft.Tag.Get("ltsv") 106 | if i := strings.IndexByte(key, ','); i >= 0 { 107 | key = key[:i] 108 | } 109 | if key == "-" { 110 | continue 111 | } 112 | if key == "" { 113 | key = strings.ToLower(ft.Name) 114 | } 115 | s, ok := l[key] 116 | if !ok { 117 | continue 118 | } 119 | potentiallyNull := s == "-" || s == "" 120 | if fv.Kind() == reflect.Ptr { 121 | if fv.IsNil() { 122 | if potentiallyNull { 123 | switch fv.Type().Elem().Kind() { 124 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: 125 | continue 126 | } 127 | } 128 | fv.Set(reflect.New(fv.Type().Elem())) 129 | } 130 | fv = fv.Elem() 131 | } 132 | if !fv.CanSet() { 133 | continue 134 | } 135 | 136 | switch fv.Kind() { 137 | case reflect.String: 138 | fv.SetString(s) 139 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 140 | if potentiallyNull { 141 | continue 142 | } 143 | i, err := strconv.ParseInt(s, 10, 64) 144 | if err != nil || fv.OverflowInt(i) { 145 | errs[ft.Name] = &UnmarshalTypeError{"number " + s, fv.Type()} 146 | continue 147 | } 148 | fv.SetInt(i) 149 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 150 | if potentiallyNull { 151 | continue 152 | } 153 | i, err := strconv.ParseUint(s, 10, 64) 154 | if err != nil || fv.OverflowUint(i) { 155 | errs[ft.Name] = &UnmarshalTypeError{"number " + s, fv.Type()} 156 | continue 157 | } 158 | fv.SetUint(i) 159 | case reflect.Float32, reflect.Float64: 160 | if potentiallyNull { 161 | continue 162 | } 163 | n, err := strconv.ParseFloat(s, fv.Type().Bits()) 164 | if err != nil || fv.OverflowFloat(n) { 165 | errs[ft.Name] = &UnmarshalTypeError{"number " + s, fv.Type()} 166 | continue 167 | } 168 | fv.SetFloat(n) 169 | case reflect.Bool: 170 | if potentiallyNull { 171 | continue 172 | } 173 | b, err := strconv.ParseBool(s) 174 | if err != nil { 175 | errs[ft.Name] = &UnmarshalTypeError{Value: "bool " + s, Type: fv.Type()} 176 | continue 177 | } 178 | fv.SetBool(b) 179 | default: 180 | u := indirect(fv) 181 | if u == nil { 182 | errs[ft.Name] = &UnmarshalTypeError{s, fv.Type()} 183 | } else { 184 | err := u.UnmarshalText([]byte(s)) 185 | if err != nil { 186 | errs[ft.Name] = err 187 | } 188 | } 189 | } 190 | } 191 | 192 | if len(errs) < 1 { 193 | return nil 194 | } 195 | return errs 196 | } 197 | 198 | func indirect(v reflect.Value) encoding.TextUnmarshaler { 199 | if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { 200 | v = v.Addr() 201 | } 202 | for { 203 | if v.Kind() == reflect.Interface && !v.IsNil() { 204 | e := v.Elem() 205 | if e.Kind() == reflect.Ptr && !e.IsNil() && e.Elem().Kind() == reflect.Ptr { 206 | v = e 207 | continue 208 | } 209 | } 210 | if v.Kind() != reflect.Ptr { 211 | break 212 | } 213 | if v.Elem().Kind() != reflect.Ptr && v.CanSet() { 214 | break 215 | } 216 | if v.IsNil() { 217 | v.Set(reflect.New(v.Type().Elem())) 218 | } 219 | if v.Type().NumMethod() > 0 { 220 | if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { 221 | return u 222 | } 223 | } 224 | } 225 | return nil 226 | } 227 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | package ltsv 2 | 3 | import ( 4 | "bytes" 5 | "encoding" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | // MarshalError is an error type for Marshal() 15 | type MarshalError map[string]error 16 | 17 | func (m MarshalError) Error() string { 18 | if len(m) == 0 { 19 | return "(no error)" 20 | } 21 | 22 | ee := make([]string, 0, len(m)) 23 | for name, err := range m { 24 | ee = append(ee, fmt.Sprintf("field %q: %s", name, err)) 25 | } 26 | 27 | return strings.Join(ee, "\n") 28 | } 29 | 30 | // OfField returns the error correspoinding to a given field 31 | func (m MarshalError) OfField(name string) error { 32 | err := m[name] 33 | if e, ok := err.(*MarshalTypeError); ok { 34 | if e.err != nil { 35 | return e.err 36 | } 37 | } 38 | return m[name] 39 | } 40 | 41 | // An MarshalTypeError describes a LTSV value that was 42 | // not appropriate for a value of a specific Go type. 43 | type MarshalTypeError struct { 44 | Value string 45 | Type reflect.Type 46 | key string 47 | err error 48 | } 49 | 50 | func (e *MarshalTypeError) Error() string { 51 | if e.err != nil { 52 | return e.err.Error() 53 | } 54 | return fmt.Sprintf("ltsv: failed to marshal type: %s, value: %s", e.Type.String(), e.Value) 55 | } 56 | 57 | var keyDelim = []byte{':'} 58 | var valDelim = []byte{'\t'} 59 | 60 | type fieldWriter func(w io.Writer, v reflect.Value) error 61 | 62 | func makeStructWriter(v reflect.Value) fieldWriter { 63 | t := v.Type() 64 | n := t.NumField() 65 | 66 | writers := make([]fieldWriter, n) 67 | for i := 0; i < n; i++ { 68 | ft := t.Field(i) 69 | key := ft.Tag.Get("ltsv") 70 | if i := strings.IndexByte(key, ','); i >= 0 { 71 | key = key[:i] 72 | } 73 | if key == "-" { 74 | continue 75 | } 76 | if key == "" { 77 | key = strings.ToLower(ft.Name) 78 | } 79 | kind := ft.Type.Kind() 80 | 81 | dereference := false 82 | if kind == reflect.Ptr { 83 | kind = ft.Type.Elem().Kind() 84 | dereference = true 85 | } 86 | 87 | var writer fieldWriter 88 | switch kind { 89 | case reflect.String: 90 | writer = makeStringWriter(key) 91 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 92 | writer = makeIntWriter(key) 93 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 94 | writer = makeUintWriter(key) 95 | case reflect.Float32, reflect.Float64: 96 | writer = makeFloatWriter(key) 97 | case reflect.Bool: 98 | writer = makeBoolWriter(key) 99 | default: 100 | dereference = false 101 | writer = makeInterfaceWriter(key) 102 | } 103 | if i > 0 { 104 | writer = withDelimWriter(writer) 105 | } 106 | if dereference { 107 | writer = elemWriter(writer) 108 | } 109 | writers[i] = writer 110 | } 111 | 112 | return fieldWriter(func(w io.Writer, v reflect.Value) error { 113 | errs := make(MarshalError) 114 | err := writers[0](w, v.Field(0)) 115 | if err != nil { 116 | if e, ok := err.(*MarshalTypeError); ok { 117 | errs[e.key] = e 118 | } 119 | } 120 | 121 | for i, wr := range writers[1:] { 122 | if wr == nil { 123 | continue 124 | } 125 | err := wr(w, v.Field(i+1)) 126 | if err != nil { 127 | if e, ok := err.(*MarshalTypeError); ok { 128 | errs[e.key] = e 129 | } 130 | } 131 | } 132 | if len(errs) > 0 { 133 | return errs 134 | } 135 | return nil 136 | }) 137 | } 138 | 139 | func withDelimWriter(writer fieldWriter) fieldWriter { 140 | return fieldWriter(func(w io.Writer, v reflect.Value) error { 141 | w.Write(valDelim) 142 | return writer(w, v) 143 | }) 144 | } 145 | 146 | func elemWriter(writer fieldWriter) fieldWriter { 147 | return fieldWriter(func(w io.Writer, v reflect.Value) error { 148 | if v.IsNil() { 149 | return nil 150 | } 151 | return writer(w, v.Elem()) 152 | }) 153 | } 154 | 155 | func writeField(w io.Writer, key, value string) { 156 | io.WriteString(w, key) 157 | w.Write(keyDelim) 158 | io.WriteString(w, value) 159 | } 160 | 161 | func makeStringWriter(key string) fieldWriter { 162 | return fieldWriter(func(w io.Writer, v reflect.Value) error { 163 | writeField(w, key, v.String()) 164 | return nil 165 | }) 166 | } 167 | 168 | func makeIntWriter(key string) fieldWriter { 169 | return fieldWriter(func(w io.Writer, v reflect.Value) error { 170 | writeField(w, key, strconv.FormatInt(v.Int(), 10)) 171 | return nil 172 | }) 173 | } 174 | 175 | func makeUintWriter(key string) fieldWriter { 176 | return fieldWriter(func(w io.Writer, v reflect.Value) error { 177 | writeField(w, key, strconv.FormatUint(v.Uint(), 10)) 178 | return nil 179 | }) 180 | } 181 | 182 | func makeFloatWriter(key string) fieldWriter { 183 | return fieldWriter(func(w io.Writer, v reflect.Value) error { 184 | writeField(w, key, strconv.FormatFloat(v.Float(), 'f', -1, v.Type().Bits())) 185 | return nil 186 | }) 187 | } 188 | 189 | func makeBoolWriter(key string) fieldWriter { 190 | return fieldWriter(func(w io.Writer, v reflect.Value) error { 191 | writeField(w, key, strconv.FormatBool(v.Bool())) 192 | return nil 193 | }) 194 | } 195 | 196 | func makeInterfaceWriter(key string) fieldWriter { 197 | return fieldWriter(func(w io.Writer, v reflect.Value) error { 198 | if !v.CanInterface() { 199 | return &MarshalTypeError{key: key, Type: v.Type(), Value: v.String()} 200 | } 201 | 202 | switch u := v.Interface().(type) { 203 | case encoding.TextMarshaler: 204 | b, err := u.MarshalText() 205 | if err != nil { 206 | return &MarshalTypeError{key: key, Type: v.Type(), Value: v.String(), err: err} 207 | } 208 | io.WriteString(w, key) 209 | w.Write(keyDelim) 210 | w.Write(b) 211 | return nil 212 | default: 213 | return &MarshalTypeError{key: key, Type: v.Type(), Value: v.String()} 214 | } 215 | }) 216 | } 217 | 218 | type writerCache struct { 219 | cache map[reflect.Type]fieldWriter 220 | sync.RWMutex 221 | } 222 | 223 | func (c *writerCache) Get(v reflect.Value) fieldWriter { 224 | c.RLock() 225 | t := v.Type() 226 | if v, ok := c.cache[t]; ok { 227 | c.RUnlock() 228 | return v 229 | } 230 | c.RUnlock() 231 | writer := makeStructWriter(v) 232 | 233 | c.Lock() 234 | c.cache[t] = writer 235 | c.Unlock() 236 | 237 | return writer 238 | } 239 | 240 | var cache = &writerCache{ 241 | cache: make(map[reflect.Type]fieldWriter), 242 | } 243 | 244 | func marshalMapTo(w io.Writer, m map[string]string) error { 245 | first := true 246 | for k, v := range m { 247 | if !first { 248 | w.Write(valDelim) 249 | } 250 | first = false 251 | writeField(w, k, v) 252 | } 253 | return nil 254 | } 255 | 256 | func marshalStructTo(w io.Writer, rv reflect.Value) error { 257 | writer := cache.Get(rv) 258 | return writer(w, rv) 259 | } 260 | 261 | // MarshalTo writes the LTSV encoding of v into w. 262 | // Be aware that the writing into w is not thread safe. 263 | func MarshalTo(w io.Writer, v interface{}) error { 264 | rv := reflect.ValueOf(v) 265 | if rv.Kind() == reflect.Ptr { 266 | rv = rv.Elem() 267 | } 268 | 269 | var err error 270 | switch rv.Kind() { 271 | case reflect.Map: 272 | if m, ok := v.(map[string]string); ok { 273 | err = marshalMapTo(w, m) 274 | break 275 | } 276 | err = fmt.Errorf("not a map[string]string") 277 | case reflect.Struct: 278 | err = marshalStructTo(w, rv) 279 | default: 280 | err = fmt.Errorf("not a struct/map: %v", v) 281 | } 282 | return err 283 | } 284 | 285 | // Marshal returns the LTSV encoding of v 286 | func Marshal(v interface{}) ([]byte, error) { 287 | w := bytes.NewBuffer(nil) 288 | err := MarshalTo(w, v) 289 | return w.Bytes(), err 290 | } 291 | --------------------------------------------------------------------------------