github.com/polygon-io/client-go@v1.16.4/rest/encoder/encoder.go (about) 1 package encoder 2 3 import ( 4 "fmt" 5 "net/url" 6 "strings" 7 "time" 8 9 "github.com/go-playground/form/v4" 10 "github.com/go-playground/validator/v10" 11 "github.com/polygon-io/client-go/rest/models" 12 ) 13 14 // Encoder defines a path and query param encoder that plays nicely with the Polygon REST API. 15 type Encoder struct { 16 validate *validator.Validate 17 pathEncoder *form.Encoder 18 queryEncoder *form.Encoder 19 } 20 21 // New returns a new path and query param encoder. 22 func New() *Encoder { 23 return &Encoder{ 24 validate: validator.New(), 25 pathEncoder: newEncoder("path"), 26 queryEncoder: newEncoder("query"), 27 } 28 } 29 30 // EncodeParams encodes path and query params and returns a valid request URI. 31 func (e *Encoder) EncodeParams(path string, params any) (string, error) { 32 if err := e.validateParams(params); err != nil { 33 return "", err 34 } 35 36 uri, err := e.encodePath(path, params) 37 if err != nil { 38 return "", err 39 } 40 41 query, err := e.encodeQuery(params) 42 if err != nil { 43 return "", err 44 } else if query != "" { 45 uri += "?" + query 46 } 47 48 return uri, nil 49 } 50 51 func (e *Encoder) validateParams(params any) error { 52 if err := e.validate.Struct(params); err != nil { 53 return fmt.Errorf("invalid request params: %w", err) 54 } 55 return nil 56 } 57 58 func (e *Encoder) encodePath(uri string, params any) (string, error) { 59 val, err := e.pathEncoder.Encode(¶ms) 60 if err != nil { 61 return "", fmt.Errorf("error encoding path params: %w", err) 62 } 63 64 pathParams := map[string]string{} 65 for k, v := range val { 66 pathParams[k] = v[0] // only accept the first one for a given key 67 } 68 69 for k, v := range pathParams { 70 uri = strings.ReplaceAll(uri, fmt.Sprintf("{%s}", k), url.PathEscape(v)) 71 } 72 73 return uri, nil 74 } 75 76 func (e *Encoder) encodeQuery(params any) (string, error) { 77 query, err := e.queryEncoder.Encode(¶ms) 78 if err != nil { 79 return "", fmt.Errorf("error encoding query params: %w", err) 80 } 81 return query.Encode(), nil 82 } 83 84 func newEncoder(tag string) *form.Encoder { 85 e := form.NewEncoder() 86 e.SetMode(form.ModeExplicit) 87 e.SetTagName(tag) 88 89 e.RegisterCustomTypeFunc(func(x any) ([]string, error) { 90 return []string{fmt.Sprint(time.Time(x.(models.Time)).Format("2006-01-02T15:04:05.000Z"))}, nil 91 }, models.Time{}) 92 e.RegisterCustomTypeFunc(func(x any) ([]string, error) { 93 return []string{fmt.Sprint(time.Time(x.(models.Date)).Format("2006-01-02"))}, nil 94 }, models.Date{}) 95 e.RegisterCustomTypeFunc(func(x any) ([]string, error) { 96 return []string{fmt.Sprint(time.Time(x.(models.Millis)).UnixMilli())}, nil 97 }, models.Millis{}) 98 e.RegisterCustomTypeFunc(func(x any) ([]string, error) { 99 if isDay(time.Time(x.(models.Nanos))) { 100 // endpoints that have nanosecond timestamp query parameters are expected to 101 // also work with date strings if a user wants all data from a specific day 102 return []string{fmt.Sprint(time.Time(x.(models.Nanos)).Format("2006-01-02"))}, nil 103 } 104 return []string{fmt.Sprint(time.Time(x.(models.Nanos)).UnixNano())}, nil 105 }, models.Nanos{}) 106 107 return e 108 } 109 110 func isDay(t time.Time) bool { 111 if t.Hour() != 0 || t.Minute() != 0 || t.Second() != 0 || t.Nanosecond() != 0 { 112 return false 113 } 114 return true 115 }