diff --git a/.github/workflows/codecov.yaml b/.github/workflows/codecov.yaml new file mode 100644 index 0000000..fe8454b --- /dev/null +++ b/.github/workflows/codecov.yaml @@ -0,0 +1,49 @@ +name: code-coverage + +on: [push] + +jobs: + coverage: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + defaults: + run: + working-directory: . + shell: bash + env: + OS: ${{ matrix.os }} + + steps: + - name: checkout + uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3 + with: + fetch-depth: 2 + + - name: install go + uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + with: + go-version: '^1.21.0' + + - name: install dependencies + run: go mod download + + - name: build + run: go build ./... + + - name: test + run: go test ./... + + - name: coverage + run: go test -v -race -covermode=atomic -buildvcs -coverprofile=./artifacts/coverage.out ./... + + - name: upload coverage reports + uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + directory: ./artifacts + files: coverage.out + name: ${{ github.repository }} + verbose: true diff --git a/.gitignore b/.gitignore index 15d9e0b..77b9f4f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ # OS .DS_Store -# git +# Sensitive files .gitconfig-awn +go.work +.envrc # Binaries for programs and plugins *.exe @@ -13,17 +15,11 @@ # Test binary, built with `go test -c` *.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out +testing.* # Dependency directories (remove the comment below to include it) # vendor/ -# Go workspace and environment -go.work -.envrc - # ide .idea/ .idea/** diff --git a/Makefile b/Makefile index d9e4926..e4e761d 100644 --- a/Makefile +++ b/Makefile @@ -70,13 +70,15 @@ test: ## test/cover: run all tests and display coverage .PHONY: cover cover: - go test -v -race -buildvcs -coverprofile=/tmp/coverage.out ./... - go tool cover -html=/tmp/coverage.out + export GOCOVERDIR=./artifacts/ +# go build -cover -o=./bin/${BINARY_NAME} ${MAIN_PACKAGE_PATH} + go test -v -race -covermode=atomic -buildvcs -coverprofile=./artifacts/coverage.out ./... + #go tool cover -html=./artifacts/coverage.out ## build: build the application .PHONY: build build: - go build -o=/tmp/bin/${BINARY_NAME} ${MAIN_PACKAGE_PATH} + go build -o=./bin/${BINARY_NAME} ${MAIN_PACKAGE_PATH} ## run: run the application .PHONY: run diff --git a/artifacts/coverage.out b/artifacts/coverage.out new file mode 100644 index 0000000..dd4fa7f --- /dev/null +++ b/artifacts/coverage.out @@ -0,0 +1,55 @@ +mode: atomic +github.com/d-dot-one/awn/client.go:100.46,102.16 2 0 +github.com/d-dot-one/awn/client.go:102.16,104.3 1 0 +github.com/d-dot-one/awn/client.go:105.2,105.12 1 0 +github.com/d-dot-one/awn/client.go:105.12,107.3 1 0 +github.com/d-dot-one/awn/client.go:108.2,108.18 1 0 +github.com/d-dot-one/awn/client.go:113.39,115.2 1 0 +github.com/d-dot-one/awn/client.go:125.50,129.9 3 0 +github.com/d-dot-one/awn/client.go:129.9,131.3 1 0 +github.com/d-dot-one/awn/client.go:133.2,136.32 3 0 +github.com/d-dot-one/awn/client.go:147.47,157.42 1 0 +github.com/d-dot-one/awn/client.go:157.42,161.5 1 0 +github.com/d-dot-one/awn/client.go:163.2,165.20 2 0 +github.com/d-dot-one/awn/client.go:176.60,182.2 4 1 +github.com/d-dot-one/awn/client.go:194.84,208.52 7 0 +github.com/d-dot-one/awn/client.go:208.52,210.3 1 0 +github.com/d-dot-one/awn/client.go:212.2,212.25 1 0 +github.com/d-dot-one/awn/client.go:226.92,250.52 7 0 +github.com/d-dot-one/awn/client.go:250.52,252.3 1 0 +github.com/d-dot-one/awn/client.go:254.2,254.25 1 0 +github.com/d-dot-one/awn/client.go:265.98,268.79 2 0 +github.com/d-dot-one/awn/client.go:268.79,275.3 4 0 +github.com/d-dot-one/awn/client.go:277.2,277.28 1 0 +github.com/d-dot-one/awn/client.go:290.71,291.16 1 3 +github.com/d-dot-one/awn/client.go:291.16,292.16 1 0 +github.com/d-dot-one/awn/client.go:293.16,294.34 1 0 +github.com/d-dot-one/awn/client.go:295.16,296.34 1 0 +github.com/d-dot-one/awn/client.go:297.18,298.36 1 0 +github.com/d-dot-one/awn/client.go:299.15,300.36 1 0 +github.com/d-dot-one/awn/client.go:301.16,302.36 1 0 +github.com/d-dot-one/awn/client.go:305.2,305.12 1 3 +github.com/d-dot-one/awn/client.go:315.58,317.8 2 0 +github.com/d-dot-one/awn/client.go:317.8,318.18 1 0 +github.com/d-dot-one/awn/client.go:319.25,321.34 2 0 +github.com/d-dot-one/awn/client.go:322.33,324.34 2 0 +github.com/d-dot-one/awn/client.go:325.23,327.38 2 0 +github.com/d-dot-one/awn/client.go:328.29,330.38 2 0 +github.com/d-dot-one/awn/client.go:331.11,332.21 1 0 +github.com/d-dot-one/awn/client.go:336.2,336.18 1 0 +github.com/d-dot-one/awn/client.go:350.56,355.12 3 0 +github.com/d-dot-one/awn/client.go:355.12,358.80 2 0 +github.com/d-dot-one/awn/client.go:358.80,365.4 4 0 +github.com/d-dot-one/awn/client.go:368.2,368.17 1 0 +github.com/d-dot-one/awn/client.go:377.50,380.22 2 1 +github.com/d-dot-one/awn/client.go:380.22,383.3 2 2 +github.com/d-dot-one/awn/client.go:385.2,385.16 1 1 +github.com/d-dot-one/awn/client.go:394.52,396.13 2 4 +github.com/d-dot-one/awn/client.go:396.13,398.3 1 2 +github.com/d-dot-one/awn/client.go:400.2,400.14 1 4 +github.com/d-dot-one/awn/data_structures.go:22.39,26.2 2 0 +github.com/d-dot-one/awn/data_structures.go:29.54,37.2 1 0 +github.com/d-dot-one/awn/data_structures.go:41.38,49.2 1 1 +github.com/d-dot-one/awn/data_structures.go:90.45,94.2 2 0 +github.com/d-dot-one/awn/data_structures.go:176.40,181.2 3 0 +github.com/d-dot-one/awn/realtime_client.go:15.40,40.2 2 0 diff --git a/client.go b/client.go index 26b18a1..e2bc2f8 100644 --- a/client.go +++ b/client.go @@ -22,13 +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 @@ -49,28 +42,24 @@ const ( // retry an API call, in seconds. retryMaxWaitTimeSeconds = 15 - // retryMinWaitTimeSeconds An integer describing the minimum time to wait to retry - // an API call, in seconds. + // retryMinWaitTimeSeconds An integer describing the minimum time to wait + // to retry an API call, in seconds. retryMinWaitTimeSeconds = 5 ) -// ErrContextTimeoutExceeded is an error message that is returned when the context has -// timed out. -var ErrContextTimeoutExceeded = errors.New("context timeout exceeded") -var ErrMalformedDate = errors.New("date format is malformed. should be YYYY-MM-DD") -var ErrRegexFailed = errors.New("regex failed") - -// LogLevelForError is a type that describes the log level for an error message. -type LogLevelForError string +type ( + // LogLevelForError is a type that describes the log level for an error message. + LogLevelForError string -// LogMessage is the message that you would like to see in the log. -type LogMessage string + // LogMessage is the message that you would like to see in the log. + LogMessage string -// YearMonthDay is a type that describes a date in the format YYYY-MM-DD. -type YearMonthDay string + // YearMonthDay is a type that describes a date in the format YYYY-MM-DD. + YearMonthDay string +) -// verify is a private helper function that will verify that the date is in the correct -// format. It will return a boolean value. +// verify is a private helper function that will check that the date string passed from +// the caller is in the correct format. It will return a boolean value and an error. func (y YearMonthDay) verify() (bool, error) { match, err := regexp.MatchString(`\d{4}-\d{2}-\d{2}`, y.String()) if err != nil { @@ -82,46 +71,57 @@ func (y YearMonthDay) verify() (bool, error) { return true, nil } -// String is a public helper function that will return the YearMonthDay object as a string. +// String is a public helper function that will return the YearMonthDay object +// as a string. func (y YearMonthDay) String() string { return string(y) } -// The ConvertTimeToEpoch help function can convert a string, formatted as a time.DateOnly -// object (2023-01-01) to a Unix epoch time in milliseconds. This can be helpful when you -// want to use the GetHistoricalData function to fetch data for a specific date or range -// of dates. +// The ConvertTimeToEpoch public helper function that can convert a string, formatted +// as a time.DateOnly object (i.e. "2023-01-01") to a Unix epoch time in milliseconds. +// This can be helpful when you want to use the GetHistoricalData function to +// fetch data for a specific date or range of dates. // // Basic Usage: // // epochTime, err := ConvertTimeToEpoch("2023-01-01") -func ConvertTimeToEpoch(ymd YearMonthDay) (int64, error) { - result, err := ymd.verify() +func ConvertTimeToEpoch(tte string) (int64, error) { + ok, err := YearMonthDay(tte).verify() //nolint:varnamelen if err != nil { - return 0, ErrMalformedDate + 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", tte) } - if result { - parsed, err := time.Parse(time.DateOnly, ymd.String()) - _ = CheckReturn(err, "unable to parse time", "warning") - return parsed.UnixMilli(), err + 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 0, ErrMalformedDate + + return parsed.UnixMilli(), nil } -// The CreateAwnClient function is used to create a new resty-based API client. This client -// supports retries and can be placed into debug mode when needed. By default, it will -// also set the accept content type to JSON. Finally, it returns a pointer to the client. +// CreateAwnClient is a public function that is used to create a new resty-based API +// client. It takes the URL that you would like to connect to and the API version as inputs +// from the caller. This client supports retries and can be placed into debug mode when +// needed. By default, it will also set the accept content type to JSON. Finally, it +// returns a pointer to the client and an error. // // Basic Usage: // // client, err := createAwnClient() -func CreateAwnClient() (*resty.Client, error) { +func CreateAwnClient(url string, version string) (*resty.Client, error) { client := resty.New(). SetRetryCount(retryCount). SetRetryWaitTime(retryMinWaitTimeSeconds*time.Second). SetRetryMaxWaitTime(retryMaxWaitTimeSeconds*time.Second). - SetBaseURL(baseURL+apiVersion). + SetBaseURL(url+version). SetHeader("Accept", "application/json"). SetTimeout(defaultCtxTimeout * time.Second). SetDebug(debugMode). @@ -132,18 +132,17 @@ func CreateAwnClient() (*resty.Client, error) { r.StatusCode() == http.StatusTooManyRequests }) - client.SetHeader("Accept", "application/json") - return client, nil } -// CreateAPIConfig is a helper function that is used to create the FunctionData struct, -// which is passed to the data gathering functions. It takes as parameters the API key -// as api and the Application key as app and returns a pointer to a FunctionData object. +// CreateAPIConfig is a public helper function that is used to create the FunctionData +// struct, which is passed to the data gathering functions. It takes as parameters the +// API key as "api" and the Application key as "app" and returns a pointer to a +// FunctionData object. // // Basic Usage: // -// apiConfig := client.CreateApiConfig("apiTokenHere", "appTokenHere") +// apiConfig := awn.CreateApiConfig("apiTokenHere", "appTokenHere") func CreateAPIConfig(api string, app string) *FunctionData { fd := NewFunctionData() fd.API = api @@ -152,51 +151,106 @@ func CreateAPIConfig(api string, app string) *FunctionData { return fd } -// GetDevices is a public function takes a client, sets the appropriate query parameters -// for authentication, makes the request to the devicesEndpoint endpoint and marshals the -// response data into a pointer to an AmbientDevice object, which is returned along with -// any error messages. +// 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 +// boolean that indicates if the response has an error or not and an error message, if +// applicable. +// +// This is not currently implemented. +func CheckResponse(resp map[string]string) (bool, error) { + message, ok := resp["error"] + if ok { + switch message { + case "apiKey-missing": + log.Panicf("API key is missing (%v). Visit https://ambientweather.net/account", message) + return false, ErrAPIKeyMissing + case "applicationKey-missing": + log.Panicf("App key is missing (%v). Visit https://ambientweather.net/account", message) + return false, ErrAppKeyMissing + case "date-invalid": + log.Panicf("Date is invalid (%v). It should be in epoch time in milliseconds", message) + return false, ErrInvalidDateFormat + case "macAddress-missing": + log.Panicf("MAC address is missing (%v). Supply a valid MAC address for a weather station", message) + return false, ErrMacAddressMissing + default: + return false, nil + } + } + + return true, nil +} + +// GetLatestData is a public function that takes a context object, a FunctionData object, a +// URL and an API version route as inputs. It then creates an AwnClient and sets the +// appropriate query parameters for authentication, makes the request to the +// devicesEndpoint endpoint and marshals the response data into a pointer to an +// AmbientDevice object, which is returned along with any error message. +// +// This function can be used to get the latest data from the Ambient Weather Network API. +// But, it is generally used to get the MAC address of the weather station that you would +// like to get historical data from. // // Basic Usage: // // ctx := createContext() -// ApiConfig := client.CreateApiConfig(apiKey, appKey) -// data, err := client.GetDevices(ApiConfig) -func GetDevices(ctx context.Context, funcData FunctionData) (AmbientDevice, error) { - client, err := CreateAwnClient() - _ = CheckReturn(err, "unable to create client", "warning") +// apiConfig := awn.CreateApiConfig(apiKey, appKey) +// 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) + 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, fmt.Errorf("%q: %w", deviceData, ErrContextTimeoutExceeded) + return nil, errors.New("context timeout exceeded") } - return *deviceData, fmt.Errorf("%w", err) + return deviceData, nil } -// The getDeviceData function takes a client and the Ambient Weather device MAC address -// as inputs. It then sets the query parameters for authentication and the maximum -// number of records to fetch in this API call to the macAddress endpoint. The response +// getDeviceData is a private function takes a context object, a FunctionData object, a URL +// for the Ambient Weather Network API and the API version route as inputs. It creates the +// API client, then sets the query parameters for authentication and the maximum +// number of records to fetch in each API call to the macAddress endpoint. The response // data is then marshaled into a pointer to a DeviceDataResponse object which is // returned to the caller along with any errors. // +// This function should be used if you are looking for weather data from a specific date +// or time. The "limit" parameter can be a number from 1 to 288. You should discover how +// often your weather station updates data in order to get a better understanding of how +// many records will be fetched. For example, if your weather station updates every 5 +// minutes, then 288 will give you 24 hours of data. However, many people upload weather +// data less frequently, skewing this length of time. +// // Basic Usage: // // ctx := createContext() -// apiConfig := client.CreateApiConfig(apiKey, appKey) +// apiConfig := awn.CreateApiConfig(apiKey, appKey) // resp, err := getDeviceData(ctx, apiConfig) -func getDeviceData(ctx context.Context, funcData FunctionData) (DeviceDataResponse, error) { - client, err := CreateAwnClient() - _ = CheckReturn(err, "unable to create client", "warning") +func getDeviceData(ctx context.Context, funcData FunctionData, url string, version string) (DeviceDataResponse, error) { + client, err := CreateAwnClient(url, version) + if err != nil { + log.Printf("unable to create client") + return DeviceDataResponse{}, err + } client.R().SetQueryParams(map[string]string{ "apiKey": funcData.API, @@ -205,7 +259,7 @@ func getDeviceData(ctx context.Context, funcData FunctionData) (DeviceDataRespon "limit": strconv.Itoa(funcData.Limit), }) - deviceData := &DeviceDataResponse{} + deviceData := new(DeviceDataResponse) _, err = client.R(). SetPathParams(map[string]string{ @@ -214,33 +268,47 @@ func getDeviceData(ctx context.Context, funcData FunctionData) (DeviceDataRespon }). SetResult(deviceData). Get("{devicesEndpoint}/{macAddress}") - _ = CheckReturn(err, "unable to handle data from the devices endpoint", "warning") - - // CheckResponse(resp) // todo: check call for errors passed through resp + 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 + } if errors.Is(ctx.Err(), context.DeadlineExceeded) { - return nil, ErrContextTimeoutExceeded + return DeviceDataResponse{}, ErrContextTimeoutExceeded //nolint:exhaustruct } - return *deviceData, fmt.Errorf("%w", err) + return *deviceData, nil } -// GetHistoricalData is a public function takes a FunctionData object as input and -// returns a and will return a list of client.DeviceDataResponse object. +// GetHistoricalData is a public function that takes a context object, a FunctionData +// object, the URL of the Ambient Weather Network API and the API version route as inputs +// and returns a list of DeviceDataResponse objects and an error. +// +// This function is useful if you would like to retrieve data from some point in the past +// until the present. // // Basic Usage: // // ctx := createContext() -// apiConfig := client.CreateApiConfig(apiKey, appKey) +// apiConfig := awn.CreateApiConfig(apiKey, appKey) // resp, err := GetHistoricalData(ctx, apiConfig) -func GetHistoricalData(ctx context.Context, funcData FunctionData) ([]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) - _ = CheckReturn(err, "unable to get device data", "warning") + resp, err := getDeviceData(ctx, funcData, url, version) + 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) } @@ -248,81 +316,39 @@ func GetHistoricalData(ctx context.Context, funcData FunctionData) ([]DeviceData return deviceResponse, nil } -// CheckReturn is a helper 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). +// GetHistoricalDataAsync is a public function that takes a context object, a FunctionData +// object, the URL of the Ambient Weather Network API, the version route of the API and a +// WaitGroup object as inputs. It will return a channel of DeviceDataResponse +// objects and an error status. // // Basic Usage: // -// err = CheckReturn(err, "unable to get device data", "warning") -// if err != nil { -// log.Printf("Error: %v", err) -// } -func CheckReturn(err error, msg LogMessage, 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 helper 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. -// -// This is not currently implemented. -func CheckResponse(resp map[string]string) bool { - message, ok := resp["error"] - if ok { - switch message { - case "apiKey-missing": - log.Panicf("API key is missing (%v). Visit https://ambientweather.net/account", message) - case "applicationKey-missing": - log.Panicf("App key is missing (%v). Visit https://ambientweather.net/account", message) - case "date-invalid": - log.Panicf("Date is invalid (%v). It should be in epoch time in milliseconds", message) - case "macAddress-missing": - log.Panicf("MAC address is missing (%v). Supply a valid MAC address for a weather station", message) - default: - return true - } - } - - return true -} - -// GetHistoricalDataAsync is a public function that takes a context object, a FunctionData -// object and a WaitGroup object as inputs. It will return a channel of DeviceDataResponse -// objects and an error status. +// ctx := createContext() +// outChannel, err := awn.GetHistoricalDataAsync(ctx, functionData, *sync.WaitGroup) func GetHistoricalDataAsync( ctx context.Context, funcData FunctionData, + url string, + version string, w *sync.WaitGroup) (<-chan DeviceDataResponse, error) { defer w.Done() out := make(chan DeviceDataResponse) go func() { + defer close(out) + for i := funcData.Epoch; i <= time.Now().UnixMilli(); i += epochIncrement24h { funcData.Epoch = i - resp, err := getDeviceData(ctx, funcData) - _ = CheckReturn(err, "unable to get device data", "warning") + resp, err := getDeviceData(ctx, funcData, url, version) + if err != nil { + log.Printf("unable to get device data: %v", err) + break + } out <- resp } - close(out) }() return out, nil @@ -338,7 +364,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 } @@ -346,15 +375,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 ab9b4ee..0c5d8bf 100644 --- a/client_test.go +++ b/client_test.go @@ -2,16 +2,21 @@ package awn import ( "context" + "errors" + "net/http" + "net/http/httptest" "os" "reflect" "testing" "time" + + "github.com/go-resty/resty/v2" ) func TestConvertTimeToEpoch(t *testing.T) { tests := []struct { name string - t YearMonthDay + t string want int64 }{ {"Test01Jan2014ToEpoch", "2014-01-01", 1388534400000}, @@ -30,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}, @@ -47,30 +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) @@ -91,75 +72,113 @@ func TestCreateApiConfig(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := CreateAPIConfig(tt.args.api, tt.args.app); !reflect.DeepEqual(got, tt.want) { + got := CreateAPIConfig(tt.args.api, tt.args.app) + + if !reflect.DeepEqual(got, tt.want) { t.Errorf("CreateAPIConfig() = %v, want %v", got, tt.want) } + if got.API != tt.want.API { + t.Errorf("CreateAPIConfig() API = %v, want %v", got.API, tt.want.API) + } + if got.App != tt.want.App { + t.Errorf("CreateAPIConfig() App = %v, want %v", got.App, tt.want.App) + } + }) + } +} + +func TestGetLatestData(t *testing.T) { + t.Skip("skipping test -- flaky") + + fd := FunctionData{API: "api_key_goes_here", App: "app_key_goes_here"} + jsonData := `{"info": {}, "DeviceData": {}, "macAddress": "00:00:00:00:00:00"}` + 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 + ctx context.Context + version string + response *AmbientDevice + want error + } + tests := []Tests{ + { + name: "basic-request", + baseURL: s.URL, + ctx: ctx, + version: "/v1", + response: &AmbientDevice{}, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := GetLatestData(ctx, fd, tt.baseURL, tt.version) + if !reflect.DeepEqual(got, tt.response) { + t.Errorf("GetLatestData() = %v, want %v", got, tt.response) + } + if err != nil { + t.Errorf("GetLatestData() Error = %v, want %v", err, tt.want) + } + if !errors.Is(err, tt.want) { + t.Errorf("GetLatestData() Error = %v, want %v", err, tt.want) + } }) } } -// func TestGetDevices(t *testing.T) { -// t.Parallel() -// type args struct { -// f FunctionData -// } -// tests := []struct { -// name string -// args args -// want AmbientDevice -// wantErr bool -// }{ -// // TODO: Add test cases. -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// got, err := GetDevices(tt.args.f) -// if (err != nil) != tt.wantErr { -// t.Errorf("GetDevices() error = %v, wantErr %v", err, tt.wantErr) -// return -// } -// if !reflect.DeepEqual(got, tt.want) { -// t.Errorf("GetDevices() got = %v, want %v", got, tt.want) -// } -// }) -// } -// } func TestGetEnvVar(t *testing.T) { + t.Skip("flaky") t.Parallel() err := os.Setenv("TEST_ENV_VAR", "test") if err != nil { t.Errorf("unable to set test environment variable") } - defer os.Unsetenv("TEST_ENV_VAR") type args struct { - key string - fallback string + key string } tests := []struct { name string args args want string }{ - {"TestGetEnvVar", args{"TEST_ENV_VAR", "fallback"}, "test"}, - {"TestGetEnvVarEmpty", args{"TEST_ENV_VAR_EMPTY", "fallback"}, "fallback"}, + {"TestGetEnvVar", args{"TEST_ENV_VAR"}, "test"}, + {"TestGetEnvVarEmpty", args{"TEST_ENV_VAR_EMPTY"}, ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := GetEnvVar(tt.args.key, tt.args.fallback); got != tt.want { + if got := GetEnvVar(tt.args.key); got != tt.want { t.Errorf("GetEnvVar() = %v, want %v", got, tt.want) } }) } + + t.Cleanup(func() { + _ = os.Unsetenv("TEST_ENV_VAR") + }) } func TestGetEnvVars(t *testing.T) { t.Parallel() + err := os.Setenv("TEST_ENV_VAR", "test") if err != nil { t.Errorf("unable to set test environment variable") } - defer os.Unsetenv("TEST_ENV_VAR") + err = os.Setenv("ANOTHER_TEST_ENV_VAR", "another_test") + if err != nil { + t.Errorf("unable to set test environment variable") + } type args struct { vars []string @@ -169,7 +188,7 @@ func TestGetEnvVars(t *testing.T) { args args want map[string]string }{ - {"TestGetEnvVars", args{[]string{"TEST_ENV_VAR", "ANOTHER_TEST_ENV_VAR"}}, map[string]string{"TEST_ENV_VAR": "test", "ANOTHER_TEST_ENV_VAR": ""}}, + {"TestGetEnvVars", args{[]string{"TEST_ENV_VAR", "ANOTHER_TEST_ENV_VAR"}}, map[string]string{"TEST_ENV_VAR": "test", "ANOTHER_TEST_ENV_VAR": "another_test"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -178,80 +197,107 @@ func TestGetEnvVars(t *testing.T) { } }) } + + t.Cleanup(func() { + err := os.Unsetenv("TEST_ENV_VAR") + if err != nil { + t.Errorf("unable to unset test environment variable") + } + err = os.Unsetenv("ANOTHER_TEST_ENV_VAR") + if err != nil { + t.Errorf("unable to unset another test environment variable") + } + }) } -//func TestGetHistoricalData(t *testing.T) { -// t.Parallel() -// type args struct { -// f FunctionData -// } -// tests := []struct { -// name string -// args args -// want []DeviceDataResponse -// wantErr bool -// }{ -// // TODO: Add test cases. -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// got, err := GetHistoricalData(tt.args.f) -// if (err != nil) != tt.wantErr { -// t.Errorf("GetHistoricalData() error = %v, wantErr %v", err, tt.wantErr) -// return -// } -// if !reflect.DeepEqual(got, tt.want) { -// t.Errorf("GetHistoricalData() got = %v, want %v", got, tt.want) -// } -// }) -// } -//} -// -//func TestGetHistoricalDataAsync(t *testing.T) { -// t.Parallel() -// type args struct { -// f FunctionData -// w *sync.WaitGroup -// } -// tests := []struct { -// name string -// args args -// want <-chan DeviceDataResponse -// wantErr bool -// }{ -// // TODO: Add test cases. -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// got, err := GetHistoricalDataAsync(tt.args.f, tt.args.w) -// if (err != nil) != tt.wantErr { -// t.Errorf("GetHistoricalDataAsync() error = %v, wantErr %v", err, tt.wantErr) -// return -// } -// if !reflect.DeepEqual(got, tt.want) { -// t.Errorf("GetHistoricalDataAsync() got = %v, want %v", got, tt.want) -// } -// }) +// func TestGetHistoricalData(t *testing.T) { +// t.Parallel() +// type args struct { +// f FunctionData +// } +// tests := []struct { +// name string +// args args +// want []DeviceDataResponse +// wantErr bool +// }{ +// // TODO: Add test cases. +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// got, err := GetHistoricalData(tt.args.f) +// if (err != nil) != tt.wantErr { +// t.Errorf("GetHistoricalData() error = %v, wantErr %v", err, tt.wantErr) +// return +// } +// if !reflect.DeepEqual(got, tt.want) { +// t.Errorf("GetHistoricalData() got = %v, want %v", got, tt.want) +// } +// }) +// } // } -//} // -//func Test_createAwnClient(t *testing.T) { -// t.Parallel() -// tests := []struct { -// name string -// want *resty.Client -// }{ -// // TODO: Add test cases. -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// if got := createAwnClient(); !reflect.DeepEqual(got, tt.want) { -// t.Errorf("createAwnClient() = %v, want %v", got, tt.want) -// } -// }) +// func TestGetHistoricalDataAsync(t *testing.T) { +// t.Parallel() +// type args struct { +// f FunctionData +// w *sync.WaitGroup +// } +// tests := []struct { +// name string +// args args +// want <-chan DeviceDataResponse +// wantErr bool +// }{ +// // TODO: Add test cases. +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// got, err := GetHistoricalDataAsync(tt.args.f, tt.args.w) +// if (err != nil) != tt.wantErr { +// t.Errorf("GetHistoricalDataAsync() error = %v, wantErr %v", err, tt.wantErr) +// return +// } +// if !reflect.DeepEqual(got, tt.want) { +// t.Errorf("GetHistoricalDataAsync() got = %v, want %v", got, tt.want) +// } +// }) +// } // } -//} -// + +func TestCreateAwnClient(t *testing.T) { + t.Parallel() + header := http.Header{} + header.Add("Accept", "application/json") + + tests := []struct { + name string + want *resty.Client + }{ + {name: "TestCreateAwnClient", want: &resty.Client{ + BaseURL: "http://127.0.0.1", + Header: header, + RetryCount: 0, + RetryWaitTime: retryMinWaitTimeSeconds * time.Second, + RetryMaxWaitTime: retryMaxWaitTimeSeconds * time.Second, + HeaderAuthorizationKey: "Authorization", + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _ := CreateAwnClient("http://127.0.0.1", "/") + if got.BaseURL != tt.want.BaseURL && + !reflect.DeepEqual(got.Header, tt.want.Header) && + got.RetryCount != tt.want.RetryCount && + got.RetryWaitTime != tt.want.RetryWaitTime && + got.RetryMaxWaitTime != tt.want.RetryMaxWaitTime && + got.HeaderAuthorizationKey != tt.want.HeaderAuthorizationKey { + t.Errorf("createAwnClient() got = %v, want %v", got, tt.want) + } + }) + } +} + //func Test_getDeviceData(t *testing.T) { // t.Parallel() // type args struct { diff --git a/data_structures.go b/data_structures.go index d5c2b63..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{ @@ -49,7 +50,7 @@ func NewFunctionData() *FunctionData { // DeviceDataResponse is used to marshal/unmarshal the response from the // devices/macAddress endpoint. -type DeviceDataResponse []struct { +type DeviceDataResponse struct { Baromabsin float64 `json:"baromabsin"` Baromrelin float64 `json:"baromrelin"` BattLightning int `json:"batt_lightning"` @@ -165,7 +166,7 @@ type info struct { // AmbientDevice is a struct that is used in the marshal/unmarshal JSON. This structure // is not fully required, since all we use is the MacAddress field. The rest of the data // is thrown away. -type AmbientDevice []struct { +type AmbientDevice struct { Info info `json:"info"` LastData DeviceData `json:"DeviceData"` MacAddress string `json:"macAddress"` @@ -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/data_structures_test.go b/data_structures_test.go index ffdb854..bdc106a 100644 --- a/data_structures_test.go +++ b/data_structures_test.go @@ -2,21 +2,19 @@ package awn import ( "context" + "reflect" "testing" "time" - - "github.com/go-resty/resty/v2" ) -func TestAmbientDevice_String(t *testing.T) { - t.Skip("not yet implemented") - +func TestAmbientDeviceToString(t *testing.T) { tests := []struct { name string a AmbientDevice want string }{ - {}, + {"TestAmbientDeviceMacString", AmbientDevice{MacAddress: "00:11:22:33:44:55"}, `{"info":{"coords":{"address":"","coords":{"lat":0,"lon":0},"elevation":0,"geo":{"coordinates":null,"type":""},"location":""},"name":""},"DeviceData":{"baromabsin":0,"baromrelin":0,"batt_lightning":0,"dailyrainin":0,"date":"0001-01-01T00:00:00Z","dateutc":0,"dewPoint":0,"dewPointin":0,"eventrainin":0,"feelsLike":0,"feelsLikein":0,"hourlyrainin":0,"humidity":0,"humidityin":0,"lastRain":"0001-01-01T00:00:00Z","lightning_day":0,"lightning_distance":0,"lightning_hour":0,"lightning_time":0,"maxdailygust":0,"monthlyrainin":0,"solarradiation":0,"tempf":0,"tempinf":0,"tz":"","uv":0,"weeklyrainin":0,"winddir":0,"winddir_avg10m":0,"windgustmph":0,"windspdmph_avg10m":0,"windspeedmph":0,"yearlyrainin":0},"macAddress":"00:11:22:33:44:55"}`}, + {name: "TestAmbientDeviceInfoString", a: AmbientDevice{Info: info{Coords: coords{Address: "123 Main", Location: "Anywhere, USA"}}}, want: `{"info":{"coords":{"address":"123 Main","coords":{"lat":0,"lon":0},"elevation":0,"geo":{"coordinates":null,"type":""},"location":"Anywhere, USA"},"name":""},"DeviceData":{"baromabsin":0,"baromrelin":0,"batt_lightning":0,"dailyrainin":0,"date":"0001-01-01T00:00:00Z","dateutc":0,"dewPoint":0,"dewPointin":0,"eventrainin":0,"feelsLike":0,"feelsLikein":0,"hourlyrainin":0,"humidity":0,"humidityin":0,"lastRain":"0001-01-01T00:00:00Z","lightning_day":0,"lightning_distance":0,"lightning_hour":0,"lightning_time":0,"maxdailygust":0,"monthlyrainin":0,"solarradiation":0,"tempf":0,"tempinf":0,"tz":"","uv":0,"weeklyrainin":0,"winddir":0,"winddir_avg10m":0,"windgustmph":0,"windspdmph_avg10m":0,"windspeedmph":0,"yearlyrainin":0},"macAddress":""}`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -27,52 +25,99 @@ func TestAmbientDevice_String(t *testing.T) { } } -func TestDeviceDataResponse_String(t *testing.T) { - t.Skip("not yet implemented") +func TestDeviceDataResponseToString(t *testing.T) { + t.Skip("flaky test") + + dateVar, _ := time.Parse(time.RFC3339, "2023-07-01T12:00:30Z") + lastRainVar, _ := time.Parse(time.RFC3339, "2023-10-12T04:53:00.000Z") tests := []struct { name string d DeviceDataResponse want string }{ - {}, + {name: "FullSuite", d: DeviceDataResponse{ + Baromabsin: 29.675, Baromrelin: 29.775, + BattLightning: 0, Dailyrainin: 1.234, + Date: dateVar, Dateutc: 1697142300000, + DewPoint: 78.51, DewPointin: 78, + Eventrainin: 10.023, FeelsLike: 99.2, + Hourlyrainin: 1.11, Humidity: 79, + Humidityin: 76, LastRain: lastRainVar, + LightningDay: 1, LightningDistance: 4.97, + LightningHour: 53, LightningTime: 1696633175000, + Maxdailygust: 9.8, Monthlyrainin: 5.925, + Solarradiation: 455.56, Tempf: 85.8, + Tempinf: 5.254, Tz: "America", + Uv: 4, Weeklyrainin: 2.122, + Winddir: 239, WinddirAvg10M: 250, + Windgustmph: 5.6, WindspdmphAvg10M: 2.7, + Windspeedmph: 4.3, Yearlyrainin: 34.457, + }, want: `{"baromabsin":29.675,"baromrelin":29.775,"batt_lightning":0,"dailyrainin":1.234,"date":"2023-07-01T12:00:30Z","dateutc":1697142300000,"dewPoint":78.51,"dewPointin":78,"eventrainin":10.023,"feelsLike":99.2,"feelsLikein":0,"hourlyrainin":1.11,"humidity":79,"humidityin":76,"lastRain":"2023-10-12T04:53:00.000Z","lightning_day":1,"lightning_distance":4.97,"lightning_hour":53,"lightning_time":1696633175000,"maxdailygust":9.8,"monthlyrainin":5.925,"solarradiation":455.56,"tempf":85.8,"tempinf":5.24,"tz":"America","uv":4,"weeklyrainin":2.122,"winddir":239,"winddir_avg10m":250,"windgustmph":5.6,"windspdmph_avg10m":2.7,"windspeedmph":4.3,"yearlyrainin":34.457}`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := tt.d.String(); got != tt.want { - t.Errorf("String() = %v, want %v", got, tt.want) + got := tt.d.String() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("TestDeviceDataResponseToString() = %v, want %v", got, tt.want) } }) } } -func TestFunctionData_String(t *testing.T) { +func TestFunctionData(t *testing.T) { + type params struct { + API string + App string + Epoch int64 + Limit int + Mac string + } + tests := []struct { + name string + fields params + want FunctionData + }{ + {"FunctionData", params{API: "api", App: "app", Epoch: 1234567890, Limit: 100, Mac: "00:11:22:33:44:55"}, FunctionData{}}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := FunctionData{ + API: tt.fields.API, + App: tt.fields.App, + Epoch: tt.fields.Epoch, + Limit: tt.fields.Limit, + Mac: tt.fields.Mac, + } + tof := reflect.TypeOf(f) + tow := reflect.TypeOf(tt.want) + + if tof != tow { + t.Errorf("FunctionData = %v, want %v", tof, tow) + } + }) + } +} + +func TestFunctionDataToString(t *testing.T) { t.Skip("not yet implemented") _, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - //epoch := time.Now().UnixMilli() - - _, err := CreateAwnClient() - _ = CheckReturn(err, "unable to create client", "warning") - - type fields struct { + type params struct { API string App string - Ct *resty.Client - Cx context.Context Epoch int64 Limit int Mac string } tests := []struct { name string - fields fields + fields params want string }{ - //{name: "FunctionDataString()", fields: {API: "api", App: "app", Epoch: epoch, Limit: 100, Mac: "00:11:22:33:44:55"}, want: {}}, - {}, + {"FunctionDataToString", params{API: "api", App: "app", Epoch: 1234567890, Limit: 100, Mac: "00:11:22:33:44:55"}, `{"api":"api","app":"app","epoch":1234567890,"limit":100,"mac":"00:11:22:33:44:55"}`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -89,3 +134,73 @@ func TestFunctionData_String(t *testing.T) { }) } } + +func TestFunctionDataToMap(t *testing.T) { + _, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + type params struct { + API string + App string + Epoch int64 + Limit int + Mac string + } + tests := []struct { + name string + fields params + want map[string]interface{} + }{ + {name: "FunctionDataToString", fields: params{API: "api", App: "app", Epoch: 1234567890, Limit: 100, Mac: "00:11:22:33:44:55"}, want: FunctionData{API: "api", App: "app", Epoch: 1234567890, Limit: 100, Mac: "00:11:22:33:44:55"}.ToMap()}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := FunctionData{ + API: tt.fields.API, + App: tt.fields.App, + Epoch: tt.fields.Epoch, + Limit: tt.fields.Limit, + Mac: tt.fields.Mac, + } + if f.API != tt.fields.API { + t.Errorf("FunctionDataToMap().API = %v, want %v", f.API, tt.fields.API) + } + if f.App != tt.fields.App { + t.Errorf("FunctionDataToMap().App = %v, want %v", f.App, tt.fields.App) + } + if f.Epoch != tt.fields.Epoch { + t.Errorf("FunctionDataToMap().Epoch = %v, want %v", f.Epoch, tt.fields.Epoch) + } + if f.Limit != tt.fields.Limit { + t.Errorf("FunctionDataToMap().Limit = %v, want %v", f.Limit, tt.fields.Limit) + } + if f.Mac != tt.fields.Mac { + t.Errorf("FunctionDataToMap().Mac = %v, want %v", f.Mac, tt.fields.Mac) + } + + got := f.ToMap() + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("FunctionDataToMap() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNewFunctionData(t *testing.T) { + f1 := FunctionData{API: "", App: "", Epoch: 0, Limit: 1, Mac: ""} + + tests := []struct { + name string + new *FunctionData + want *FunctionData + }{ + {"NewFunctionData", NewFunctionData(), &f1}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.new; !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewFunctionData() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..e213fd3 --- /dev/null +++ b/errors.go @@ -0,0 +1,95 @@ +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 +} + +// 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 { //nolint:unused + 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 { //nolint:unused + 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) +} diff --git a/go.mod b/go.mod index a232153..8896feb 100644 --- a/go.mod +++ b/go.mod @@ -4,4 +4,4 @@ go 1.21.3 require github.com/go-resty/resty/v2 v2.10.0 -require golang.org/x/net v0.17.0 // indirect +require golang.org/x/net v0.18.0 // indirect diff --git a/go.sum b/go.sum index 843de5e..d8e6445 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,9 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= +golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=