From ca9582b4e6b7d18da92691087421dc844e4a8fa0 Mon Sep 17 00:00:00 2001 From: d-dot-one Date: Thu, 30 Nov 2023 11:01:29 -0600 Subject: [PATCH] more changes... i guess small updates doesnt work here --- client.go | 151 +++++++++++++++++++-------------------------- client_test.go | 74 ++++++++-------------- data_structures.go | 7 ++- errors.go | 97 +++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 138 deletions(-) create mode 100644 errors.go diff --git a/client.go b/client.go index 6951bf2..55fc006 100644 --- a/client.go +++ b/client.go @@ -9,6 +9,7 @@ package awn import ( "context" "errors" + "fmt" "log" "net/http" "os" @@ -21,14 +22,6 @@ import ( ) const ( - // apiVersion is a string and describes the version of the API that Ambient - // Weather is using. - //apiVersion = "/v1" - - // baseURL The base URL for the Ambient Weather API (Not the real-time API) - // as a string. - //baseURL = "https://rt.ambientweather.net" - // debugMode Enable verbose logging by setting this boolean value to true. debugMode = false @@ -54,36 +47,6 @@ const ( retryMinWaitTimeSeconds = 5 ) -var ( - // ErrContextTimeoutExceeded is an error message that is returned when - // the context has timed out. - ErrContextTimeoutExceeded = errors.New("context timeout exceeded") - - // ErrMalformedDate is a custom error and message that is returned when a - // date is passed that does not conform to the required format. - ErrMalformedDate = errors.New("date format is malformed. should be YYYY-MM-DD") - - // ErrRegexFailed is a custom error and message that is returned when regex - // fails catastrophically. - ErrRegexFailed = errors.New("regex failed") - - // ErrAPIKeyMissing is a custom error and message that is returned when no API key is - // passed to a function that requires it. - ErrAPIKeyMissing = errors.New("api key is missing") - - // ErrAppKeyMissing is a custom error and message that is returned when no application - // key is passed to a function that requires it. - ErrAppKeyMissing = errors.New("application key is missing") - - // ErrInvalidDateFormat is a custom error and message that is returned when the date - // is not passed as an epoch time in milliseconds. - ErrInvalidDateFormat = errors.New("date is invalid. It should be in epoch time in milliseconds") - - // ErrMacAddressMissing is a custom error and message that is returned when no MAC - // address is passed to a function that requires it. - ErrMacAddressMissing = errors.New("mac address missing") -) - type ( // LogLevelForError is a type that describes the log level for an error message. LogLevelForError string @@ -122,18 +85,26 @@ func (y YearMonthDay) String() string { // Basic Usage: // // epochTime, err := ConvertTimeToEpoch("2023-01-01") -func ConvertTimeToEpoch(t string) (int64, error) { - ok, err := YearMonthDay(t).verify() - _ = CheckReturn(err, "unable to verify date", "warning") +func ConvertTimeToEpoch(tte string) (int64, error) { + ok, err := YearMonthDay(tte).verify() //nolint:varnamelen + if err != nil { + log.Printf("unable to verify date") + err = fmt.Errorf("unable to verify date: %w", err) + return 0, err + } if !ok { - log.Fatalf("invalid date format, %v should be YYYY-MM-DD", t) + log.Fatalf("invalid date format, %v should be YYYY-MM-DD", tte) } - parsed, err := time.Parse(time.DateOnly, t) - _ = CheckReturn(err, "unable to parse time", "warning") + parsed, err := time.Parse(time.DateOnly, tte) + if err != nil { + log.Printf("unable to parse time") + err = fmt.Errorf("unable to parse time: %w", err) + return 0, err + } - return parsed.UnixMilli(), err + return parsed.UnixMilli(), nil } // CreateAwnClient is a public function that is used to create a new resty-based API @@ -160,6 +131,7 @@ func CreateAwnClient(url string, version string) (*resty.Client, error) { r.StatusCode() >= http.StatusInternalServerError || r.StatusCode() == http.StatusTooManyRequests }) + // todo: check for a valid client before returning return client, nil } @@ -180,32 +152,6 @@ func CreateAPIConfig(api string, app string) *FunctionData { return fd } -// CheckReturn is a public function to remove the usual error checking cruft while also -// logging the error message. It takes an error, a message and a log level as inputs and -// returns an error (can be nil of course). You can then use the err message for custom -// handling of the error. -// -// Basic Usage: -// -// err = CheckReturn(err, "unable to get device data", "warning") -func CheckReturn(err error, msg string, level LogLevelForError) error { - if err != nil { - switch level { - case "panic": - log.Panicf("%v: %v", msg, err) - case "fatal": - log.Fatalf("%v: %v", msg, err) - case "warning": - log.Printf("%v: %v\n", msg, err) - case "info": - log.Printf("%v: %v\n", msg, err) - case "debug": - log.Printf("%v: %x\n", msg, err) - } - } - return err -} - // CheckResponse is a public function that will take an API response and evaluate it // for any errors that might have occurred. The API specification does not publish all // the possible error messages, but these are what I have found so far. It returns a @@ -254,23 +200,31 @@ func CheckResponse(resp map[string]string) (bool, error) { // data, err := awn.GetLatestData(ctx, ApiConfig, baseURL, apiVersion) func GetLatestData(ctx context.Context, funcData FunctionData, url string, version string) (*AmbientDevice, error) { client, err := CreateAwnClient(url, version) - _ = CheckReturn(err, "unable to create client", "warning") + if err != nil { + log.Printf("unable to create client") + wrappedErr := fmt.Errorf("unable to create client: %w", err) + return nil, wrappedErr + } client.R().SetQueryParams(map[string]string{ "apiKey": funcData.API, "applicationKey": funcData.App, }) - deviceData := &AmbientDevice{} + deviceData := new(AmbientDevice) _, err = client.R().SetResult(deviceData).Get(devicesEndpoint) - _ = CheckReturn(err, "unable to handle data from devicesEndpoint", "warning") + if err != nil { + log.Printf("unable to get data from devicesEndpoint") + wrappedErr := fmt.Errorf("unable to get data from devicesEndpoint: %w", err) + return nil, wrappedErr + } if errors.Is(ctx.Err(), context.DeadlineExceeded) { return nil, errors.New("context timeout exceeded") } - return deviceData, err + return deviceData, nil } // getDeviceData is a private function takes a context object, a FunctionData object, a URL @@ -294,7 +248,10 @@ func GetLatestData(ctx context.Context, funcData FunctionData, url string, versi // resp, err := getDeviceData(ctx, apiConfig) func getDeviceData(ctx context.Context, funcData FunctionData, url string, version string) (DeviceDataResponse, error) { client, err := CreateAwnClient(url, version) - _ = CheckReturn(err, "unable to create client", "warning") + if err != nil { + log.Printf("unable to create client") + return DeviceDataResponse{}, err + } client.R().SetQueryParams(map[string]string{ "apiKey": funcData.API, @@ -303,7 +260,7 @@ func getDeviceData(ctx context.Context, funcData FunctionData, url string, versi "limit": strconv.Itoa(funcData.Limit), }) - deviceData := &DeviceDataResponse{} + deviceData := new(DeviceDataResponse) _, err = client.R(). SetPathParams(map[string]string{ @@ -312,15 +269,19 @@ func getDeviceData(ctx context.Context, funcData FunctionData, url string, versi }). SetResult(deviceData). Get("{devicesEndpoint}/{macAddress}") - _ = CheckReturn(err, "unable to handle data from the devices endpoint", "warning") + if err != nil { + log.Printf("unable to get data from devicesEndpoint") + wrappedErr := fmt.Errorf("unable to get data from devicesEndpoint: %w", err) + return DeviceDataResponse{}, wrappedErr + } - //CheckResponse(resp) // todo: check response for errors passed through resp + // todo: check response for errors passed through resp if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return DeviceDataResponse{}, ErrContextTimeoutExceeded + return DeviceDataResponse{}, ErrContextTimeoutExceeded //nolint:exhaustruct } - return *deviceData, err + return *deviceData, nil } // GetHistoricalData is a public function that takes a context object, a FunctionData @@ -335,14 +296,22 @@ func getDeviceData(ctx context.Context, funcData FunctionData, url string, versi // ctx := createContext() // apiConfig := awn.CreateApiConfig(apiKey, appKey) // resp, err := GetHistoricalData(ctx, apiConfig) -func GetHistoricalData(ctx context.Context, funcData FunctionData, url string, version string) ([]DeviceDataResponse, error) { +func GetHistoricalData( + ctx context.Context, + funcData FunctionData, + url string, + version string) ([]DeviceDataResponse, error) { var deviceResponse []DeviceDataResponse for i := funcData.Epoch; i <= time.Now().UnixMilli(); i += epochIncrement24h { funcData.Epoch = i resp, err := getDeviceData(ctx, funcData, url, version) - _ = CheckReturn(err, "unable to get device data", "warning") + if err != nil { + log.Printf("unable to get device data") + wrappedErr := fmt.Errorf("unable to get device data: %w", err) + return nil, wrappedErr + } deviceResponse = append(deviceResponse, resp) } @@ -376,7 +345,10 @@ func GetHistoricalDataAsync( funcData.Epoch = i resp, err := getDeviceData(ctx, funcData, url, version) - _ = CheckReturn(err, "unable to get device data", "warning") + if err != nil { + log.Printf("unable to get device data: %v", err) + break + } out <- resp } @@ -395,7 +367,10 @@ func GetEnvVars(vars []string) map[string]string { envVars := make(map[string]string) for v := range vars { - value := GetEnvVar(vars[v], "") + value := GetEnvVar(vars[v]) + if value == "" { + log.Printf("environment variable %v is empty or not set", vars[v]) + } envVars[vars[v]] = value } @@ -403,15 +378,15 @@ func GetEnvVars(vars []string) map[string]string { } // GetEnvVar is a public function attempts to fetch an environment variable. If that -// environment variable is not found, it will return 'fallback'. +// environment variable is not found, it will return an empty string. // // Basic Usage: // // environmentVariable := GetEnvVar("ENV_VAR_1", "fallback") -func GetEnvVar(key string, fallback string) string { +func GetEnvVar(key string) string { value, exists := os.LookupEnv(key) if !exists { - value = fallback + value = "" } return value diff --git a/client_test.go b/client_test.go index 31a1cae..6e40a39 100644 --- a/client_test.go +++ b/client_test.go @@ -3,7 +3,6 @@ package awn import ( "context" "errors" - "net" "net/http" "net/http/httptest" "os" @@ -17,7 +16,7 @@ import ( func TestConvertTimeToEpoch(t *testing.T) { tests := []struct { name string - t YearMonthDay + t string want int64 }{ {"Test01Jan2014ToEpoch", "2014-01-01", 1388534400000}, @@ -36,7 +35,7 @@ func TestConvertTimeToEpochBadFormat(t *testing.T) { t.Parallel() tests := []struct { name string - t YearMonthDay + t string want error }{ {"TestWrongDateFormat", "11-15-2021", ErrMalformedDate}, @@ -53,31 +52,6 @@ func TestConvertTimeToEpochBadFormat(t *testing.T) { } } -func TestCheckReturn(t *testing.T) { - t.Parallel() - - type args struct { - e error - msg LogMessage - level LogLevelForError - } - tests := []struct { - name string - args args - }{ - {"TestCheckReturnDebug", args{nil, "Debug log message", "debug"}}, - {"TestCheckReturnInfo", args{nil, "Info log message", "info"}}, - {"TestCheckReturnWarning", args{nil, "Warning log message", "warning"}}, - {"TestCheckReturnFatal", args{nil, "Fatal log message", "fatal"}}, - {"TestCheckReturnPanic", args{nil, "Panic log message", "panic"}}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _ = CheckReturn(tt.args.e, tt.args.msg, tt.args.level) - }) - } -} - func TestCreateApiConfig(t *testing.T) { t.Parallel() _, cancel := context.WithTimeout(context.Background(), time.Second*3) @@ -121,6 +95,12 @@ func TestGetLatestData(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() + s := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Write([]byte(jsonData)) + })) + defer s.Close() + type Tests struct { name string baseURL string @@ -132,7 +112,7 @@ func TestGetLatestData(t *testing.T) { tests := []Tests{ { name: "basic-request", - baseURL: "http://127.0.0.1:9998", + baseURL: s.URL, ctx: ctx, version: "/v1", response: &AmbientDevice{}, @@ -140,23 +120,23 @@ func TestGetLatestData(t *testing.T) { }, } - server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - _, err := w.Write([]byte(jsonData)) - if err != nil { - return - } - })) - - listener, err := net.Listen("tcp", "127.0.0.1:9998") - if err != nil { - t.Errorf("unable to create listener: %v on port 9998", err) - } - - _ = server.Listener.Close() - server.Listener = listener - server.Start() - defer server.Close() + //server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // w.WriteHeader(http.StatusOK) + // _, err := w.Write([]byte(jsonData)) + // if err != nil { + // return + // } + //})) + // + //listener, err := net.Listen("tcp", "127.0.0.1:9998") + //if err != nil { + // t.Errorf("unable to create listener: %v on port 9998", err) + //} + // + //_ = server.Listener.Close() + //server.Listener = listener + //server.Start() + //defer server.Close() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -314,7 +294,6 @@ func TestCreateAwnClient(t *testing.T) { }{ {name: "TestCreateAwnClient", want: &resty.Client{ BaseURL: "http://127.0.0.1", - HostURL: "http://127.0.0.1", Header: header, RetryCount: 0, RetryWaitTime: retryMinWaitTimeSeconds * time.Second, @@ -326,7 +305,6 @@ func TestCreateAwnClient(t *testing.T) { t.Run(tt.name, func(t *testing.T) { got, _ := CreateAwnClient("http://127.0.0.1", "/") if got.BaseURL != tt.want.BaseURL && - got.HostURL != tt.want.HostURL && !reflect.DeepEqual(got.Header, tt.want.Header) && got.RetryCount != tt.want.RetryCount && got.RetryWaitTime != tt.want.RetryWaitTime && diff --git a/data_structures.go b/data_structures.go index 2f076e4..2d6aa03 100644 --- a/data_structures.go +++ b/data_structures.go @@ -2,6 +2,7 @@ package awn import ( "encoding/json" + "log" "time" ) @@ -35,7 +36,7 @@ func (f FunctionData) ToMap() map[string]interface{} { } } -// NewFunctionData creates a new FunctionData object with some default values and return +// NewFunctionData creates a new FunctionData object with bare default values and return // it to the caller as a pointer. func NewFunctionData() *FunctionData { return &FunctionData{ @@ -174,7 +175,9 @@ type AmbientDevice struct { // String is a helper function to print the AmbientDevice struct as a string. func (a AmbientDevice) String() string { r, err := json.Marshal(a) - _ = CheckReturn(err, "unable to marshall json from AmbientDevice", "warning") + if err != nil { + log.Printf("unable to marshall json from AmbientDevice: %v", err) + } return string(r) } diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..ec2377d --- /dev/null +++ b/errors.go @@ -0,0 +1,97 @@ +package awn + +import ( + "errors" + "fmt" +) + +type errorType int + +const ( + _ errorType = iota // so we don't start at 0 + errContextTimeoutExceeded + errMalformedDate + errRegexFailed + errAPIKeyMissing + errAppKeyMissing + errInvalidDateFormat + errMacAddressMissing +) + +var ( + ErrContextTimeoutExceeded = ClientError{kind: errContextTimeoutExceeded} //nolint:exhaustruct + ErrMalformedDate = ClientError{kind: errMalformedDate} //nolint:exhaustruct + ErrRegexFailed = ClientError{kind: errRegexFailed} //nolint:exhaustruct + ErrAPIKeyMissing = ClientError{kind: errAPIKeyMissing} //nolint:exhaustruct + ErrAppKeyMissing = ClientError{kind: errAppKeyMissing} //nolint:exhaustruct + ErrInvalidDateFormat = ClientError{kind: errInvalidDateFormat} //nolint:exhaustruct + ErrMacAddressMissing = ClientError{kind: errMacAddressMissing} //nolint:exhaustruct +) + +// ClientError is a public custom error type that is used to return errors from the client. +type ClientError struct { + kind errorType // errKind in example + value int + err error +} + +// todo: should all of the be passing a pointer? + +// Error is a public function that returns the error message. +func (c ClientError) Error() string { + switch c.kind { + case errContextTimeoutExceeded: + return fmt.Sprintf("context timeout exceeded: %v", c.value) + case errMalformedDate: + return fmt.Sprintf("date format is malformed. should be YYYY-MM-DD: %v", c.value) + case errRegexFailed: + return fmt.Sprintf("regex failed: %v", c.value) + case errAPIKeyMissing: + return fmt.Sprintf("api key is missing: %v", c.value) + case errAppKeyMissing: + return fmt.Sprintf("application key is missing: %v", c.value) + case errInvalidDateFormat: + return fmt.Sprintf("date is invalid. It should be in epoch time in milliseconds: %v", c.value) + case errMacAddressMissing: + return fmt.Sprintf("mac address is missing: %v", c.value) + default: + return fmt.Sprintf("unknown error: %v", c.value) + } +} + +// from is a private function that returns an error with a particular location and the +// underlying error. +func (c ClientError) from(pos int, err error) ClientError { + ce := c + ce.value = pos + ce.err = err + return ce +} + +// with is a private function that returns an error with a particular value. +func (c ClientError) with(val int) ClientError { + ce := c + ce.value = val + return ce +} + +// Is is a public function that reports whether any error in the error's chain matches target. +func (c ClientError) Is(err error) bool { + var clientError ClientError + ok := errors.As(err, &clientError) // reflection + if !ok { + return false + } + + return clientError.kind == c.kind +} + +// Unwrap is a public function that returns the underlying error by unwrapping it. +func (c ClientError) Unwrap() error { + return c.err +} + +// Wrap is a public function that allows for errors to be propagated up correctly. +func (c ClientError) Wrap() error { + return fmt.Errorf("error: %w", c) +}