1package stdlib
2
3import (
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
13var 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.
91func CSVDecode(str cty.Value) (cty.Value, error) {
92	return CSVDecodeFunc.Call([]cty.Value{str})
93}
94