]>
Commit | Line | Data |
---|---|---|
15c0b25d AP |
1 | package stdlib |
2 | ||
3 | import ( | |
4 | "encoding/csv" | |
5 | "fmt" | |
6 | "io" | |
7 | "strings" | |
8 | ||
9 | "github.com/zclconf/go-cty/cty" | |
10 | "github.com/zclconf/go-cty/cty/function" | |
11 | ) | |
12 | ||
13 | var CSVDecodeFunc = function.New(&function.Spec{ | |
14 | Params: []function.Parameter{ | |
15 | { | |
16 | Name: "str", | |
17 | Type: cty.String, | |
18 | }, | |
19 | }, | |
20 | Type: func(args []cty.Value) (cty.Type, error) { | |
21 | str := args[0] | |
22 | if !str.IsKnown() { | |
23 | return cty.DynamicPseudoType, nil | |
24 | } | |
25 | ||
26 | r := strings.NewReader(str.AsString()) | |
27 | cr := csv.NewReader(r) | |
28 | headers, err := cr.Read() | |
29 | if err == io.EOF { | |
30 | return cty.DynamicPseudoType, fmt.Errorf("missing header line") | |
31 | } | |
32 | if err != nil { | |
33 | return cty.DynamicPseudoType, err | |
34 | } | |
35 | ||
36 | atys := make(map[string]cty.Type, len(headers)) | |
37 | for _, name := range headers { | |
38 | if _, exists := atys[name]; exists { | |
39 | return cty.DynamicPseudoType, fmt.Errorf("duplicate column name %q", name) | |
40 | } | |
41 | atys[name] = cty.String | |
42 | } | |
43 | return cty.List(cty.Object(atys)), nil | |
44 | }, | |
45 | Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { | |
46 | ety := retType.ElementType() | |
47 | atys := ety.AttributeTypes() | |
48 | str := args[0] | |
49 | r := strings.NewReader(str.AsString()) | |
50 | cr := csv.NewReader(r) | |
51 | cr.FieldsPerRecord = len(atys) | |
52 | ||
53 | // Read the header row first, since that'll tell us which indices | |
54 | // map to which attribute names. | |
55 | headers, err := cr.Read() | |
56 | if err != nil { | |
57 | return cty.DynamicVal, err | |
58 | } | |
59 | ||
60 | var rows []cty.Value | |
61 | for { | |
62 | cols, err := cr.Read() | |
63 | if err == io.EOF { | |
64 | break | |
65 | } | |
66 | if err != nil { | |
67 | return cty.DynamicVal, err | |
68 | } | |
69 | ||
70 | vals := make(map[string]cty.Value, len(cols)) | |
71 | for i, str := range cols { | |
72 | name := headers[i] | |
73 | vals[name] = cty.StringVal(str) | |
74 | } | |
75 | rows = append(rows, cty.ObjectVal(vals)) | |
76 | } | |
77 | ||
78 | if len(rows) == 0 { | |
79 | return cty.ListValEmpty(ety), nil | |
80 | } | |
81 | return cty.ListVal(rows), nil | |
82 | }, | |
83 | }) | |
84 | ||
85 | // CSVDecode parses the given CSV (RFC 4180) string and, if it is valid, | |
86 | // returns a list of objects representing the rows. | |
87 | // | |
88 | // The result is always a list of some object type. The first row of the | |
89 | // input is used to determine the object attributes, and subsequent rows | |
90 | // determine the values of those attributes. | |
91 | func CSVDecode(str cty.Value) (cty.Value, error) { | |
92 | return CSVDecodeFunc.Call([]cty.Value{str}) | |
93 | } |