Skip to content

Commit 6635e89

Browse files
authored
Merge pull request #2 from deploymenttheory/dev
Dev
2 parents a127fe1 + 2feeef6 commit 6635e89

20 files changed

+738
-609
lines changed

internal/apihandlers/jamfpro/jamfpro_api_handler.go

Lines changed: 90 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ import (
4949

5050
_ "embed"
5151

52-
"github.com/deploymenttheory/go-api-http-client/internal/httpclient"
52+
"github.com/deploymenttheory/go-api-http-client/internal/errors"
53+
"github.com/deploymenttheory/go-api-http-client/internal/logger"
54+
"go.uber.org/zap"
5355
)
5456

5557
// Endpoint constants represent the URL suffixes used for Jamf API token interactions.
@@ -104,13 +106,39 @@ type EndpointConfig struct {
104106

105107
// JamfAPIHandler implements the APIHandler interface for the Jamf Pro API.
106108
type JamfAPIHandler struct {
107-
logger httpclient.Logger // logger is used to output logs for the API handling processes.
108-
OverrideBaseDomain string // OverrideBaseDomain is used to override the base domain for URL construction.
109-
InstanceName string // InstanceName is the name of the Jamf instance.
109+
OverrideBaseDomain string // OverrideBaseDomain is used to override the base domain for URL construction.
110+
InstanceName string // InstanceName is the name of the Jamf instance.
111+
}
112+
113+
type Logger interface {
114+
Debug(msg string, keysAndValues ...interface{})
115+
Info(msg string, keysAndValues ...interface{})
116+
Warn(msg string, keysAndValues ...interface{})
117+
Error(msg string, keysAndValues ...interface{})
110118
}
111119

112120
// Functions
113121

122+
func (j *JamfAPIHandler) GetDefaultBaseDomain() string {
123+
return DefaultBaseDomain
124+
}
125+
126+
func (j *JamfAPIHandler) GetOAuthTokenEndpoint() string {
127+
return OAuthTokenEndpoint
128+
}
129+
130+
func (j *JamfAPIHandler) GetBearerTokenEndpoint() string {
131+
return BearerTokenEndpoint
132+
}
133+
134+
func (j *JamfAPIHandler) GetTokenRefreshEndpoint() string {
135+
return TokenRefreshEndpoint
136+
}
137+
138+
func (j *JamfAPIHandler) GetTokenInvalidateEndpoint() string {
139+
return TokenInvalidateEndpoint
140+
}
141+
114142
// GetBaseDomain returns the appropriate base domain for URL construction.
115143
// It uses OverrideBaseDomain if set, otherwise falls back to DefaultBaseDomain.
116144
func (j *JamfAPIHandler) GetBaseDomain() string {
@@ -120,19 +148,19 @@ func (j *JamfAPIHandler) GetBaseDomain() string {
120148
return DefaultBaseDomain
121149
}
122150

123-
// ConstructAPIResourceEndpoint returns the full URL for a Jamf API resource endpoint path.
124-
func (j *JamfAPIHandler) ConstructAPIResourceEndpoint(endpointPath string) string {
151+
// ConstructAPIResourceEndpoint constructs the full URL for a Jamf API resource endpoint path and logs the URL.
152+
func (j *JamfAPIHandler) ConstructAPIResourceEndpoint(endpointPath string, log logger.Logger) string {
125153
baseDomain := j.GetBaseDomain()
126154
url := fmt.Sprintf("https://%s%s%s", j.InstanceName, baseDomain, endpointPath)
127-
j.logger.Info("Request will be made to API URL:", "URL", url)
155+
log.Info("Constructed API resource endpoint URL", zap.String("URL", url))
128156
return url
129157
}
130158

131-
// ConstructAPIAuthEndpoint returns the full URL for a Jamf API auth endpoint path.
132-
func (j *JamfAPIHandler) ConstructAPIAuthEndpoint(endpointPath string) string {
159+
// ConstructAPIAuthEndpoint constructs the full URL for a Jamf API auth endpoint path and logs the URL.
160+
func (j *JamfAPIHandler) ConstructAPIAuthEndpoint(endpointPath string, log logger.Logger) string {
133161
baseDomain := j.GetBaseDomain()
134162
url := fmt.Sprintf("https://%s%s%s", j.InstanceName, baseDomain, endpointPath)
135-
j.logger.Info("Request will be made to API authentication URL:", "URL", url)
163+
log.Info("Constructed API authentication URL", zap.String("URL", url))
136164
return url
137165
}
138166

@@ -144,36 +172,36 @@ func (j *JamfAPIHandler) ConstructAPIAuthEndpoint(endpointPath string) string {
144172
// - For url endpoints starting with "/api", it defaults to "application/json" for the JamfPro API.
145173
// If the endpoint does not match any of the predefined patterns, "application/json" is used as a fallback.
146174
// This method logs the decision process at various stages for debugging purposes.
147-
func (u *JamfAPIHandler) GetContentTypeHeader(endpoint string) string {
175+
func (u *JamfAPIHandler) GetContentTypeHeader(endpoint string, log logger.Logger) string {
148176
// Dynamic lookup from configuration should be the first priority
149177
for key, config := range configMap {
150178
if strings.HasPrefix(endpoint, key) {
151179
if config.ContentType != nil {
152-
u.logger.Debug("Content-Type for endpoint found in configMap", "endpoint", endpoint, "content_type", *config.ContentType)
180+
log.Debug("Content-Type for endpoint found in configMap", zap.String("endpoint", endpoint), zap.String("content_type", *config.ContentType))
153181
return *config.ContentType
154182
}
155-
u.logger.Debug("Content-Type for endpoint is nil in configMap, handling as special case", "endpoint", endpoint)
183+
log.Debug("Content-Type for endpoint is nil in configMap, handling as special case", zap.String("endpoint", endpoint))
156184
// If a nil ContentType is an expected case, do not set Content-Type header.
157185
return "" // Return empty to indicate no Content-Type should be set.
158186
}
159187
}
160188

161189
// If no specific configuration is found, then check for standard URL patterns.
162190
if strings.Contains(endpoint, "/JSSResource") {
163-
u.logger.Debug("Content-Type for endpoint defaulting to XML for Classic API", "endpoint", endpoint)
191+
log.Debug("Content-Type for endpoint defaulting to XML for Classic API", zap.String("endpoint", endpoint))
164192
return "application/xml" // Classic API uses XML
165193
} else if strings.Contains(endpoint, "/api") {
166-
u.logger.Debug("Content-Type for endpoint defaulting to JSON for JamfPro API", "endpoint", endpoint)
194+
log.Debug("Content-Type for endpoint defaulting to JSON for JamfPro API", zap.String("endpoint", endpoint))
167195
return "application/json" // JamfPro API uses JSON
168196
}
169197

170198
// Fallback to JSON if no other match is found.
171-
u.logger.Debug("Content-Type for endpoint not found in configMap or standard patterns, using default JSON", "endpoint", endpoint)
199+
log.Debug("Content-Type for endpoint not found in configMap or standard patterns, using default JSON", zap.String("endpoint", endpoint))
172200
return "application/json"
173201
}
174202

175203
// MarshalRequest encodes the request body according to the endpoint for the API.
176-
func (u *JamfAPIHandler) MarshalRequest(body interface{}, method string, endpoint string) ([]byte, error) {
204+
func (u *JamfAPIHandler) MarshalRequest(body interface{}, method string, endpoint string, log logger.Logger) ([]byte, error) {
177205
var (
178206
data []byte
179207
err error
@@ -195,47 +223,47 @@ func (u *JamfAPIHandler) MarshalRequest(body interface{}, method string, endpoin
195223
}
196224

197225
if method == "POST" || method == "PUT" {
198-
u.logger.Trace("XML Request Body:", "Body", string(data))
226+
log.Debug("XML Request Body", zap.String("Body", string(data)))
199227
}
200228

201229
case "json":
202230
data, err = json.Marshal(body)
203231
if err != nil {
204-
u.logger.Error("Failed marshaling JSON request", "error", err)
232+
log.Error("Failed marshaling JSON request", zap.Error(err))
205233
return nil, err
206234
}
207235

208236
if method == "POST" || method == "PUT" || method == "PATCH" {
209-
u.logger.Debug("JSON Request Body:", string(data))
237+
log.Debug("JSON Request Body", zap.String("Body", string(data)))
210238
}
211239
}
212240

213241
return data, nil
214242
}
215243

216244
// UnmarshalResponse decodes the response body from XML or JSON format depending on the Content-Type header.
217-
func (u *JamfAPIHandler) UnmarshalResponse(resp *http.Response, out interface{}) error {
245+
func (u *JamfAPIHandler) UnmarshalResponse(resp *http.Response, out interface{}, log logger.Logger) error {
218246
// Handle DELETE method
219247
if resp.Request.Method == "DELETE" {
220248
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
221249
return nil
222250
} else {
223-
return fmt.Errorf("DELETE request failed with status code: %d", resp.StatusCode)
251+
return log.Error("DELETE request failed", zap.Int("Status Code", resp.StatusCode))
224252
}
225253
}
226254

227255
bodyBytes, err := io.ReadAll(resp.Body)
228256
if err != nil {
229-
u.logger.Error("Failed reading response body", "error", err)
257+
log.Error("Failed reading response body", zap.Error(err))
230258
return err
231259
}
232260

233261
// Log the raw response body and headers
234-
u.logger.Trace("Raw HTTP Response:", string(bodyBytes))
235-
u.logger.Debug("Unmarshaling response", "status", resp.Status)
262+
log.Debug("Raw HTTP Response", zap.String("Body", string(bodyBytes)))
263+
log.Debug("Unmarshaling response", zap.String("status", resp.Status))
236264

237265
// Log headers when in debug mode
238-
u.logger.Debug("HTTP Response Headers:", resp.Header)
266+
log.Debug("HTTP Response Headers", zap.Any("Headers", resp.Header))
239267

240268
// Check the Content-Type and Content-Disposition headers
241269
contentType := resp.Header.Get("Content-Type")
@@ -249,30 +277,38 @@ func (u *JamfAPIHandler) UnmarshalResponse(resp *http.Response, out interface{})
249277
// If content type is HTML, extract the error message
250278
if strings.Contains(contentType, "text/html") {
251279
errMsg := ExtractErrorMessageFromHTML(string(bodyBytes))
252-
u.logger.Warn("Received HTML content", "error_message", errMsg, "status_code", resp.StatusCode)
253-
return &APIError{
280+
log.Warn("Received HTML content", zap.String("error_message", errMsg), zap.Int("status_code", resp.StatusCode))
281+
return &errors.APIError{
254282
StatusCode: resp.StatusCode,
255283
Message: errMsg,
256284
}
257285
}
258286

259287
// Check for non-success status codes before attempting to unmarshal
260-
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
261-
// Parse the error details from the response body for JSON content type
262-
if strings.Contains(contentType, "application/json") {
288+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
289+
// Parse the error details from the response body for JSON content type
290+
if strings.Contains(contentType, "application/json") {
263291
description, err := ParseJSONErrorResponse(bodyBytes)
264292
if err != nil {
265-
u.logger.Error("Failed to parse JSON error response", "error", err)
266-
return fmt.Errorf("received non-success status code: %d and failed to parse error response", resp.StatusCode)
293+
// Log the error using the structured logger and return the error
294+
return nil, c.Logger.Error("Failed to parse JSON error response",
295+
zap.Error(err),
296+
zap.Int("status_code", resp.StatusCode),
297+
)
267298
}
268-
return fmt.Errorf("received non-success status code: %d, error: %s", resp.StatusCode, description)
269-
}
270-
271-
// If the response is not JSON or another error occurs, return a generic error message
272-
u.logger.Error("Received non-success status code", "status_code", resp.StatusCode)
273-
return fmt.Errorf("received non-success status code: %d", resp.StatusCode)
299+
// Log the error with description using the structured logger and return the error
300+
return nil, c.Logger.Error("Received non-success status code with JSON response",
301+
zap.Int("status_code", resp.StatusCode),
302+
zap.String("error_description", description),
303+
)
274304
}
275305

306+
// If the response is not JSON or another error occurs, log a generic error message and return an error
307+
return nil, c.Logger.Error("Received non-success status code without JSON response",
308+
zap.Int("status_code", resp.StatusCode),
309+
)
310+
}
311+
276312
// Determine whether the content type is JSON or XML and unmarshal accordingly
277313
switch {
278314
case strings.Contains(contentType, "application/json"):
@@ -285,20 +321,23 @@ func (u *JamfAPIHandler) UnmarshalResponse(resp *http.Response, out interface{})
285321
}
286322

287323
// Handle any errors that occurred during unmarshaling
288-
if err != nil {
289-
// If unmarshalling fails, check if the content might be HTML
290-
if strings.Contains(string(bodyBytes), "<html>") {
324+
if err != nil {
325+
// If unmarshalling fails, check if the content might be HTML
326+
if strings.Contains(string(bodyBytes), "<html>") {
291327
errMsg := ExtractErrorMessageFromHTML(string(bodyBytes))
292-
u.logger.Warn("Received HTML content instead of expected format", "error_message", errMsg, "status_code", resp.StatusCode)
293-
return fmt.Errorf(errMsg)
294-
}
295-
296-
// Log the error and return it
297-
u.logger.Error("Failed to unmarshal response", "error", err)
298-
return fmt.Errorf("failed to unmarshal response: %v", err)
328+
329+
// Log the warning and return an error using the structured logger
330+
return nil, log.Warn("Received HTML content instead of expected format",
331+
zap.String("error_message", errMsg),
332+
zap.Int("status_code", resp.StatusCode),
333+
)
299334
}
300335

301-
return nil
336+
// Log the error using the structured logger and return the error
337+
return nil, log.Error("Failed to unmarshal response",
338+
zap.Error(err),
339+
)
340+
302341
}
303342

304343
// GetAcceptHeader constructs and returns a weighted Accept header string for HTTP requests.
@@ -327,7 +366,7 @@ func (u *JamfAPIHandler) GetAcceptHeader() string {
327366
}
328367

329368
// MarshalMultipartFormData takes a map with form fields and file paths and returns the encoded body and content type.
330-
func (u *JamfAPIHandler) MarshalMultipartRequest(fields map[string]string, files map[string]string) ([]byte, string, error) {
369+
func (u *JamfAPIHandler) MarshalMultipartRequest(fields map[string]string, files map[string]string, log logger.Logger) ([]byte, string, error) {
331370
body := &bytes.Buffer{}
332371
writer := multipart.NewWriter(body)
333372

internal/httpclient/http_error_handling.go renamed to internal/errors/http_error_handling.go

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
// http_error_handling.go
22
// This package provides utility functions and structures for handling and categorizing HTTP error responses.
3-
package httpclient
3+
package errors
44

55
import (
66
"encoding/json"
77
"fmt"
88
"net/http"
99

10+
"github.com/deploymenttheory/go-api-http-client/internal/logger"
1011
"go.uber.org/zap"
1112
)
1213

1314
// APIError represents a structured API error response.
1415
type APIError struct {
15-
StatusCode int
16-
Message string
16+
StatusCode int // HTTP status code
17+
Type string // A brief identifier for the type of error (e.g., "RateLimit", "BadRequest", etc.)
18+
Message string // Human-readable message
1719
}
1820

1921
// StructuredError represents a structured error response from the API.
@@ -24,19 +26,25 @@ type StructuredError struct {
2426
} `json:"error"`
2527
}
2628

29+
// Error returns a string representation of the APIError.
30+
func (e *APIError) Error() string {
31+
return fmt.Sprintf("API Error (Type: %s, Code: %d): %s", e.Type, e.StatusCode, e.Message)
32+
}
33+
2734
// HandleAPIError handles error responses from the API, converting them into a structured error if possible.
28-
func (c *Client) HandleAPIError(resp *http.Response) error {
35+
func HandleAPIError(resp *http.Response, log logger.Logger) error {
2936
var structuredErr StructuredError
3037
err := json.NewDecoder(resp.Body).Decode(&structuredErr)
3138
if err == nil && structuredErr.Error.Message != "" {
3239
// Using structured logging to log the structured error details
33-
c.logger.Warn("API returned structured error",
40+
log.Warn("API returned structured error",
3441
zap.String("status", resp.Status),
3542
zap.String("error_code", structuredErr.Error.Code),
3643
zap.String("error_message", structuredErr.Error.Message),
3744
)
3845
return &APIError{
3946
StatusCode: resp.StatusCode,
47+
Type: structuredErr.Error.Code,
4048
Message: structuredErr.Error.Message,
4149
}
4250
}
@@ -46,29 +54,25 @@ func (c *Client) HandleAPIError(resp *http.Response) error {
4654
if err != nil || errMsg == "" {
4755
errMsg = fmt.Sprintf("Unexpected error with status code: %d", resp.StatusCode)
4856
// Logging with structured fields
49-
c.logger.Warn("Failed to decode API error message, using default error message",
57+
log.Warn("Failed to decode API error message, using default error message",
5058
zap.String("status", resp.Status),
5159
zap.String("error_message", errMsg),
5260
)
5361
} else {
5462
// Logging non-structured error as a warning with structured fields
55-
c.logger.Warn("API returned non-structured error",
63+
log.Warn("API returned non-structured error",
5664
zap.String("status", resp.Status),
5765
zap.String("error_message", errMsg),
5866
)
5967
}
6068

6169
return &APIError{
6270
StatusCode: resp.StatusCode,
71+
Type: "UnexpectedError",
6372
Message: errMsg,
6473
}
6574
}
6675

67-
// Error returns a string representation of the APIError.
68-
func (e *APIError) Error() string {
69-
return fmt.Sprintf("API Error (Code: %d): %s", e.StatusCode, e.Message)
70-
}
71-
7276
// TranslateStatusCode provides a human-readable message for HTTP status codes.
7377
func TranslateStatusCode(statusCode int) string {
7478
messages := map[int]string{

0 commit comments

Comments
 (0)