├── README.md ├── go.mod ├── go.sum ├── main.go └── mapper ├── casetype_string.go ├── mapper.go ├── mapper_test.go ├── models.go └── struct.temp /README.md: -------------------------------------------------------------------------------- 1 | # csv2struct 2 | Easy to use library and CLI utility to generate Go struct from CSV files. 3 | As a benefit, it's fully compatible with [csvutil](https://github.com/jszwec/csvutil). 4 | So, structs generated by this utility can be used with that library. 5 | 6 | ## Install 7 | `go install github.com/Koshqua/csv2struct@latest` 8 | 9 | ## Usage 10 | 11 | ```bash 12 | NAME: 13 | csv2struct - Converts csv files to golang structs compatible with https://github.com/jszwec/csvutil 14 | 15 | USAGE: 16 | csv2struct [global options] command [command options] [arguments...] 17 | 18 | AUTHOR: 19 | Ivan Malovanyi (https://github.com/Koshqua) 20 | 21 | COMMANDS: 22 | help, h Shows a list of commands or help for one command 23 | 24 | GLOBAL OPTIONS: 25 | --from value, -f value specify which csv file to use 26 | --to value, -t value specify the output .go file 27 | --typename value, --tn value specify how to name output type 28 | --csvsep value, --cs value specify the csv separator (default: ",") 29 | --casetype value, --ct value specify the headers case type, possible values are: pascal, camel, kebab, snake, space (default: "pascal") 30 | --verbose, -v verbose logging (with debug) (default: false) 31 | --help, -h show help (default: false) 32 | ``` 33 | ## Example 34 | ```bash 35 | csv2struct -f ./test.csv -t ./blah.go -tn Blah --casetype space 36 | ``` 37 | Also, it's available as library. 38 | Will provide usage examples a bit later... 39 | 40 | 41 | ## Contribution 42 | Your contribution to the project is welcomed and appreciated. 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Koshqua/csv2struct 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/stretchr/testify v1.7.0 7 | github.com/urfave/cli/v2 v2.27.4 8 | golang.org/x/tools v0.25.0 9 | ) 10 | 11 | require ( 12 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 13 | github.com/davecgh/go-spew v1.1.0 // indirect 14 | github.com/pmezard/go-difflib v1.0.0 // indirect 15 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 16 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect 17 | golang.org/x/mod v0.21.0 // indirect 18 | golang.org/x/sync v0.8.0 // indirect 19 | golang.org/x/text v0.18.0 // indirect 20 | gopkg.in/yaml.v3 v3.0.1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 8 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 9 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 10 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 11 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 12 | github.com/urfave/cli/v2 v2.27.4 h1:o1owoI+02Eb+K107p27wEX9Bb8eqIoZCfLXloLUSWJ8= 13 | github.com/urfave/cli/v2 v2.27.4/go.mod h1:m4QzxcD2qpra4z7WhzEGn74WZLViBnMpb1ToCAKdGRQ= 14 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= 15 | github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= 16 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 17 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 18 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 19 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 20 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 21 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 22 | golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= 23 | golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 26 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | 10 | "github.com/urfave/cli/v2" 11 | 12 | "github.com/Koshqua/csv2struct/mapper" 13 | ) 14 | 15 | func main() { 16 | a := &cli.App{ 17 | Name: "csv2struct", 18 | Usage: "Converts csv files to golang structs compatible with https://github.com/jszwec/csvutil", 19 | Flags: []cli.Flag{ 20 | &cli.StringFlag{ 21 | Name: "from", 22 | Aliases: []string{"f"}, 23 | Usage: "specify which csv file to use", 24 | Required: true, 25 | }, 26 | &cli.StringFlag{ 27 | Name: "to", 28 | Aliases: []string{"t"}, 29 | Usage: "specify the output .go file", 30 | Required: true, 31 | }, 32 | &cli.StringFlag{ 33 | Name: "typename", 34 | Aliases: []string{"tn"}, 35 | Usage: "specify how to name output type", 36 | Required: false, 37 | }, 38 | &cli.StringFlag{ 39 | Name: "csvsep", 40 | Aliases: []string{"cs"}, 41 | Usage: "specify the csv separator", 42 | Value: ",", 43 | }, 44 | &cli.StringFlag{ 45 | Name: "casetype", 46 | Aliases: []string{"ct"}, 47 | Usage: "specify the headers case type, possible values are: pascal, camel, kebab, snake, space", 48 | Value: "pascal", 49 | Destination: nil, 50 | HasBeenSet: false, 51 | }, 52 | &cli.BoolFlag{ 53 | Name: "verbose", 54 | Aliases: []string{"v"}, 55 | Usage: "verbose logging (with debug)", 56 | Value: false, 57 | }, 58 | }, 59 | Authors: []*cli.Author{ 60 | { 61 | Name: "Ivan Malovanyi (https://github.com/Koshqua)", 62 | }, 63 | }, 64 | Action: func(c *cli.Context) error { 65 | return convertCsvToStruct(c) 66 | }, 67 | } 68 | err := a.Run(os.Args) 69 | if err != nil { 70 | log.Fatalln(err) 71 | } 72 | } 73 | 74 | func convertCsvToStruct(c *cli.Context) error { 75 | m := &mapper.Mapper{ 76 | Config: mapper.Config{ 77 | From: c.String("from"), 78 | To: c.String("to"), 79 | TypeName: c.String("typename"), 80 | CsvSeparator: c.String("csvsep"), 81 | WordCaseType: mapper.ParseCaseType(c.String("casetype")), 82 | Verbose: c.Bool("verbose"), 83 | AddPackageName: true, 84 | }, 85 | } 86 | 87 | parsedTemplate, err := m.CreateStructFromCsv() 88 | if err != nil { 89 | return err 90 | } 91 | f, err := os.Create(m.Config.To) 92 | if err != nil { 93 | return err 94 | } 95 | defer f.Close() 96 | _, err = f.Write([]byte(parsedTemplate)) 97 | if err != nil { 98 | return err 99 | } 100 | cmd := exec.Command("gofmt", "-w", m.Config.To) 101 | if errOut, err := cmd.CombinedOutput(); err != nil { 102 | panic(fmt.Errorf("failder to run %v: %v\n%s", strings.Join(cmd.Args, " "), err, errOut)) 103 | } 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /mapper/casetype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type CaseType"; DO NOT EDIT. 2 | 3 | package mapper 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[Pascal-1] 12 | _ = x[Camel-2] 13 | _ = x[Kebab-3] 14 | _ = x[Snake-4] 15 | _ = x[Space-5] 16 | } 17 | 18 | const _CaseType_name = "PascalCamelKebabSnakeSpace" 19 | 20 | var _CaseType_index = [...]uint8{0, 6, 11, 16, 21, 26} 21 | 22 | func (i CaseType) String() string { 23 | i -= 1 24 | if i < 0 || i >= CaseType(len(_CaseType_index)-1) { 25 | return "CaseType(" + strconv.FormatInt(int64(i+1), 10) + ")" 26 | } 27 | return _CaseType_name[_CaseType_index[i]:_CaseType_index[i+1]] 28 | } 29 | 30 | func ParseCaseType(s string) CaseType { 31 | switch s { 32 | case "pascal": 33 | return Pascal 34 | case "camel": 35 | return Camel 36 | case "kebab": 37 | return Kebab 38 | case "snake": 39 | return Snake 40 | case "space": 41 | return Space 42 | default: 43 | return Pascal 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /mapper/mapper.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "encoding/csv" 7 | "fmt" 8 | "io" 9 | "log" 10 | "os" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | "text/template" 15 | 16 | "golang.org/x/tools/go/packages" 17 | ) 18 | 19 | var fieldRe = regexp.MustCompile(`(?m)[^a-zA-Z0-9]|^\d`) 20 | 21 | type CaseType int 22 | 23 | //go:embed struct.temp 24 | var structTemp string 25 | 26 | const ( 27 | Pascal CaseType = iota + 1 28 | Camel 29 | Kebab 30 | Snake 31 | Space 32 | ) 33 | 34 | const ( 35 | typeInt = "int64" 36 | typeBool = "bool" 37 | typeString = "string" 38 | typeFloat = "float64" 39 | ) 40 | 41 | type Mapper struct { 42 | Config Config 43 | } 44 | 45 | func (m *Mapper) ParseCSV(file io.Reader) (*ParsedCSV, error) { 46 | r := csv.NewReader(file) 47 | r.Comma = []rune(m.Config.CsvSeparator)[0] 48 | r.TrimLeadingSpace = true 49 | p := new(ParsedCSV) 50 | headers, err := r.Read() 51 | if err != nil && err != io.EOF { 52 | return p, fmt.Errorf("couldn't read headers w error %v", err) 53 | } 54 | p.headers = headers 55 | values, err := r.Read() 56 | if err != nil && err != io.EOF { 57 | return p, fmt.Errorf("couldn't read values w error %v", err) 58 | } 59 | p.values = values 60 | return p, nil 61 | } 62 | 63 | func (m *Mapper) normalizeHeaders(p *ParsedCSV) ([]string, error) { 64 | switch m.Config.WordCaseType { 65 | case Pascal: 66 | normalized := make([]string, 0, len(p.headers)) 67 | for _, h := range p.headers { 68 | h = string(fieldRe.ReplaceAll([]byte(h), []byte(""))) 69 | normalized = append(normalized, h) 70 | } 71 | return normalized, nil 72 | case Camel: 73 | normalized := make([]string, 0, len(p.headers)) 74 | for _, h := range p.headers { 75 | h = string(fieldRe.ReplaceAll([]byte(h), []byte(""))) 76 | normalized = append(normalized, strings.Title(h)) 77 | } 78 | return normalized, nil 79 | case Snake: 80 | normalized := make([]string, 0, len(p.headers)) 81 | for _, h := range p.headers { 82 | words := strings.Split(h, "_") 83 | for i := 0; i < len(words); i++ { 84 | words[i] = string(fieldRe.ReplaceAll([]byte(words[i]), []byte(""))) 85 | words[i] = strings.Title(strings.ToLower(words[i])) 86 | } 87 | h = strings.Join(words, "") 88 | h = string(fieldRe.ReplaceAll([]byte(h), []byte(""))) 89 | normalized = append(normalized, h) 90 | } 91 | return normalized, nil 92 | case Kebab: 93 | normalized := make([]string, 0, len(p.headers)) 94 | for _, h := range p.headers { 95 | words := strings.Split(h, "-") 96 | for i := 0; i < len(words); i++ { 97 | words[i] = string(fieldRe.ReplaceAll([]byte(words[i]), []byte(""))) 98 | words[i] = strings.Title(strings.ToLower(words[i])) 99 | } 100 | h = strings.Join(words, "") 101 | h = string(fieldRe.ReplaceAll([]byte(h), []byte(""))) 102 | normalized = append(normalized, h) 103 | } 104 | return normalized, nil 105 | case Space: 106 | normalized := make([]string, 0, len(p.headers)) 107 | for _, h := range p.headers { 108 | words := strings.Split(h, " ") 109 | for i := 0; i < len(words); i++ { 110 | words[i] = string(fieldRe.ReplaceAll([]byte(words[i]), []byte(""))) 111 | words[i] = strings.Title(strings.ToLower(words[i])) 112 | } 113 | h = strings.Join(words, "") 114 | h = string(fieldRe.ReplaceAll([]byte(h), []byte(""))) 115 | normalized = append(normalized, h) 116 | } 117 | return normalized, nil 118 | } 119 | return nil, fmt.Errorf("non existent case") 120 | } 121 | 122 | func (m *Mapper) getFieldTypes(vals []string) []string { 123 | types := make([]string, 0, len(vals)) 124 | for _, val := range vals { 125 | if _, err := strconv.ParseBool(val); err == nil { 126 | types = append(types, typeBool) 127 | continue 128 | } 129 | if _, err := strconv.ParseInt(val, 10, 64); err == nil { 130 | types = append(types, typeInt) 131 | continue 132 | } 133 | if _, err := strconv.ParseFloat(val, 64); err == nil { 134 | types = append(types, typeFloat) 135 | continue 136 | } 137 | types = append(types, typeString) 138 | } 139 | return types 140 | } 141 | 142 | // CreateStructFromCsv is a shorthand for CreateStructFromReader, but it reads csv from file for you. 143 | func (m *Mapper) CreateStructFromCsv() (string, error) { 144 | csvFile, err := os.OpenFile(m.Config.From, os.O_RDONLY, 0666) 145 | if err != nil { 146 | return "", err 147 | } 148 | defer csvFile.Close() 149 | return m.CreateStructFromReader(csvFile) 150 | } 151 | 152 | // CreateStructFromReader reads csv from reader and return type T struct as a golang representation of that csv in reader. 153 | // It returns string in format 154 | // package p 155 | // type T struct { 156 | // fields ... 157 | // } 158 | func (m *Mapper) CreateStructFromReader(r io.Reader) (string, error) { 159 | parsed, err := m.ParseCSV(r) 160 | if err != nil { 161 | return "", err 162 | } 163 | normalizedHeader, err := m.normalizeHeaders(parsed) 164 | if err != nil { 165 | return "", err 166 | } 167 | valueTypes := m.getFieldTypes(parsed.values) 168 | if len(normalizedHeader) != len(valueTypes) && len(normalizedHeader) != len(parsed.headers) { 169 | return "", fmt.Errorf("got %v headers and %v values, not matching", len(normalizedHeader), len(valueTypes)) 170 | } 171 | pkgName := getPackageName() 172 | if pkgName == "" { 173 | pkgName = "placeholder_package_name" 174 | } 175 | typeToGen := GeneratedType{ 176 | AddPackageName: m.Config.AddPackageName, 177 | PkgName: pkgName, 178 | Name: m.Config.TypeName, 179 | } 180 | fields := make([]Field, 0, len(normalizedHeader)) 181 | for i := 0; i < len(normalizedHeader); i++ { 182 | field := Field{ 183 | Name: normalizedHeader[i], 184 | Type: valueTypes[i], 185 | CsvTag: parsed.headers[i], 186 | } 187 | fields = append(fields, field) 188 | } 189 | log.Printf("%#+v", fields) 190 | typeToGen.Fields = fields 191 | templ := template.Must(template.New("type_temp").Parse(structTemp)) 192 | w := bytes.NewBuffer([]byte{}) 193 | err = templ.Execute(w, typeToGen) 194 | return w.String(), err 195 | } 196 | 197 | func getPackageName() string { 198 | cfg := &packages.Config{ 199 | Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedName, 200 | } 201 | pkgs, err := packages.Load(cfg) 202 | if err != nil { 203 | panic(err) 204 | } 205 | if len(pkgs) != 1 { 206 | panic(fmt.Errorf("expected to get only only 1 package, got %v", len(pkgs))) 207 | } 208 | return pkgs[0].Name 209 | } 210 | -------------------------------------------------------------------------------- /mapper/mapper_test.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestMapper_parseCsv(t *testing.T) { 11 | t.Run("works with comma", func(t *testing.T) { 12 | csvEx := `name,nickname,blah 13 | ivan,koshqua,blah` 14 | m := new(Mapper) 15 | m.Config = Config{ 16 | CsvSeparator: ",", 17 | } 18 | r := bytes.NewReader([]byte(csvEx)) 19 | ps, err := m.ParseCSV(r) 20 | assert.NoError(t, err) 21 | expectedHeaders := []string{"name", "nickname", "blah"} 22 | expectedValues := []string{"ivan", "koshqua", "blah"} 23 | assert.Equal(t, expectedHeaders, ps.headers) 24 | assert.Equal(t, expectedValues, ps.values) 25 | }) 26 | t.Run("works with pipe", func(t *testing.T) { 27 | csvEx := `name|nickname|blah 28 | ivan|koshqua|blah` 29 | m := new(Mapper) 30 | m.Config = Config{ 31 | CsvSeparator: "|", 32 | } 33 | r := bytes.NewReader([]byte(csvEx)) 34 | ps, err := m.ParseCSV(r) 35 | assert.NoError(t, err) 36 | expectedHeaders := []string{"name", "nickname", "blah"} 37 | expectedValues := []string{"ivan", "koshqua", "blah"} 38 | assert.Equal(t, expectedHeaders, ps.headers) 39 | assert.Equal(t, expectedValues, ps.values) 40 | }) 41 | 42 | } 43 | 44 | func TestMapper_normalizeHeaders(t *testing.T) { 45 | type fields struct { 46 | config Config 47 | } 48 | type args struct { 49 | p *ParsedCSV 50 | } 51 | tests := []struct { 52 | name string 53 | fields fields 54 | args args 55 | want []string 56 | wantErr bool 57 | }{ 58 | { 59 | name: "pascal", 60 | fields: fields{ 61 | config: Config{ 62 | WordCaseType: Pascal, 63 | }, 64 | }, 65 | args: args{ 66 | p: &ParsedCSV{headers: []string{"1Numeric*Blah", "Abra^Kadabra"}}, 67 | }, 68 | want: []string{"NumericBlah", "AbraKadabra"}, 69 | wantErr: false, 70 | }, 71 | { 72 | name: "camel", 73 | fields: fields{ 74 | config: Config{ 75 | WordCaseType: Camel, 76 | }, 77 | }, 78 | args: args{ 79 | p: &ParsedCSV{ 80 | headers: []string{"1numeric*Bl ah", "abra Kadabra&***"}, 81 | }, 82 | }, 83 | want: []string{"NumericBlah", "AbraKadabra"}, 84 | }, 85 | { 86 | name: "kebab", 87 | fields: fields{ 88 | config: Config{ 89 | WordCaseType: Kebab, 90 | }, 91 | }, 92 | args: args{ 93 | p: &ParsedCSV{ 94 | headers: []string{"1numeric-*bl ah", "abra-kadabra&***"}, 95 | }, 96 | }, 97 | want: []string{"NumericBlah", "AbraKadabra"}, 98 | }, 99 | { 100 | name: "snake", 101 | fields: fields{ 102 | config: Config{ 103 | WordCaseType: Snake, 104 | }, 105 | }, 106 | args: args{ 107 | p: &ParsedCSV{ 108 | headers: []string{"1numeric_*bl ah", "abra_kadabra&***"}, 109 | }, 110 | }, 111 | want: []string{"NumericBlah", "AbraKadabra"}, 112 | }, 113 | { 114 | name: "space", 115 | fields: fields{ 116 | config: Config{ 117 | WordCaseType: Space, 118 | }, 119 | }, 120 | args: args{ 121 | p: &ParsedCSV{ 122 | headers: []string{"1numeric_* blah", "abra kadabra&***"}, 123 | }, 124 | }, 125 | want: []string{"NumericBlah", "AbraKadabra"}, 126 | }, 127 | { 128 | name: "non-existent", 129 | fields: fields{ 130 | config: Config{ 131 | WordCaseType: CaseType(7), 132 | }, 133 | }, 134 | args: args{ 135 | p: &ParsedCSV{ 136 | headers: []string{"1numeric_* blah", "abra kadabra&***"}, 137 | }, 138 | }, 139 | wantErr: true, 140 | }, 141 | } 142 | for _, tt := range tests { 143 | t.Run(tt.name, func(t *testing.T) { 144 | m := &Mapper{ 145 | Config: tt.fields.config, 146 | } 147 | got, err := m.normalizeHeaders(tt.args.p) 148 | if (err != nil) != tt.wantErr { 149 | t.Errorf("normalizeHeaders() error = %v, wantErr %v", err, tt.wantErr) 150 | return 151 | } 152 | assert.Equal(t, tt.want, got) 153 | }) 154 | } 155 | } 156 | 157 | func TestMapper_getFieldTypes(t *testing.T) { 158 | tests := []struct { 159 | name string 160 | vals []string 161 | want []string 162 | }{ 163 | { 164 | name: "some_types", 165 | vals: []string{"12.3", "12", "true", "false", "blah"}, 166 | want: []string{typeFloat, typeInt, typeBool, typeBool, typeString}, 167 | }, 168 | { 169 | name: "some_other", 170 | vals: []string{"112381723^12312&", "2021-02-06", "999111aaa", "????", "false1"}, 171 | want: []string{typeString, typeString, typeString, typeString, typeString}, 172 | }, 173 | } 174 | for _, tt := range tests { 175 | t.Run(tt.name, func(t *testing.T) { 176 | m := &Mapper{} 177 | got := m.getFieldTypes(tt.vals) 178 | assert.Equal(t, tt.want, got) 179 | }) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /mapper/models.go: -------------------------------------------------------------------------------- 1 | package mapper 2 | 3 | type Config struct { 4 | From, To, TypeName string 5 | CsvSeparator string 6 | WordCaseType CaseType 7 | Verbose, AddPackageName bool 8 | } 9 | 10 | type GeneratedType struct { 11 | AddPackageName bool 12 | PkgName string 13 | Name string 14 | Fields []Field 15 | } 16 | type Field struct { 17 | CsvTag string 18 | Name string 19 | Type string 20 | } 21 | 22 | type ParsedCSV struct { 23 | headers, values []string 24 | } 25 | -------------------------------------------------------------------------------- /mapper/struct.temp: -------------------------------------------------------------------------------- 1 | {{if .AddPackageName}}package {{.PkgName }}{{end}} 2 | 3 | type {{.Name}} struct { 4 | {{range .Fields}}{{.Name}} {{.Type}} `csv:"{{.CsvTag}}"` 5 | {{end -}} 6 | } --------------------------------------------------------------------------------