Skip to content

Commit b53c2e8

Browse files
committed
feat: reorganised code
1 parent bf6a62b commit b53c2e8

File tree

11 files changed

+598
-534
lines changed

11 files changed

+598
-534
lines changed

README.md

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,34 +56,42 @@ Complete implementation of the [Apple Business Manager API](https://developer.ap
5656
- **Context Support**: Context-aware operations for timeouts and cancellation
5757

5858
**Quick Start:**
59-
```go
60-
// Create JWT auth
61-
auth := client.NewJWTAuth(client.JWTAuthConfig{
62-
KeyID: "YOUR_KEY_ID",
63-
IssuerID: "YOUR_ISSUER_ID",
64-
PrivateKey: privateKey,
65-
})
6659

67-
// Create client
68-
appleClient, err := apple.NewClient(client.Config{
69-
Auth: auth,
70-
Logger: logger,
71-
Debug: true,
72-
})
73-
defer appleClient.Close()
60+
Get started quickly with multiple client setup options:
7461

75-
// Get devices with pagination
76-
devices, err := appleClient.Devices().GetOrganizationDevices(ctx, &devices.GetOrganizationDevicesOptions{
77-
Model: devices.ModeliPhone,
78-
PaginationOptions: client.PaginationOptions{Limit: 100},
62+
```go
63+
// Method 1: Simple setup with environment variables
64+
axmClient, err := axm.NewClientFromEnv()
65+
if err != nil {
66+
log.Fatalf("Failed to create client: %v", err)
67+
}
68+
69+
// Create service clients
70+
devicesClient := devices.NewClient(axmClient)
71+
deviceManagementClient := devicemanagement.NewClient(axmClient)
72+
73+
// Get organization devices
74+
ctx := context.Background()
75+
response, err := devicesClient.GetOrganizationDevices(ctx, &devices.GetOrganizationDevicesOptions{
76+
Fields: []string{
77+
devices.FieldSerialNumber,
78+
devices.FieldDeviceModel,
79+
devices.FieldStatus,
80+
},
81+
Limit: 100,
7982
})
8083

81-
// Iterate through all pages
82-
for result := range devices.Iterator(ctx) {
83-
fmt.Printf("Device: %s\n", result.Item.SerialNumber)
84+
if err != nil {
85+
log.Fatalf("Error getting devices: %v", err)
8486
}
87+
88+
fmt.Printf("Found %d devices\n", len(response.Data))
8589
```
8690

91+
📖 **[Complete Quick Start Guide →](./examples/axm/quick_start.md)**
92+
93+
The guide covers 6 different client setup methods, from simple environment variables to advanced builder patterns with full customization options.
94+
8795
### Apple Device Management API
8896

8997
Integration with [Apple Device Management](https://developer.apple.com/documentation/devicemanagement) for:

client/axm/auth.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -239,18 +239,6 @@ type OAuth2TokenResponse struct {
239239
Scope string `json:"scope"`
240240
}
241241

242-
// OAuth scope constants
243-
const (
244-
ScopeBusinessAPI = "business.api"
245-
ScopeSchoolAPI = "school.api"
246-
)
247-
248-
// Default OAuth endpoints
249-
const (
250-
DefaultOAuthTokenURL = "https://account.apple.com/auth/oauth2/token"
251-
DefaultOAuthAudience = "https://account.apple.com/auth/oauth2/v2/token"
252-
)
253-
254242
// NewOAuth2Auth creates a new OAuth 2.0 authentication provider
255243
func NewOAuth2Auth(config OAuth2Config) (*OAuth2Auth, error) {
256244
// Validate required fields

client/axm/client.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type APIResponse[T any] struct {
3838

3939
// NewClient creates a new Apple Business Manager API client
4040
func NewClient(config Config) (*Client, error) {
41-
// Set defaults
41+
4242
if config.BaseURL == "" {
4343
config.BaseURL = "https://api-business.apple.com/v1"
4444
}
@@ -58,15 +58,12 @@ func NewClient(config Config) (*Client, error) {
5858
config.Logger = zap.NewNop()
5959
}
6060

61-
// Validate required fields
6261
if config.Auth == nil {
6362
return nil, fmt.Errorf("auth provider is required")
6463
}
6564

66-
// Create Resty client with v3 best practices
6765
httpClient := resty.New()
6866

69-
// Configure client settings
7067
httpClient.
7168
SetBaseURL(config.BaseURL).
7269
SetTimeout(config.Timeout).
@@ -75,19 +72,15 @@ func NewClient(config Config) (*Client, error) {
7572
SetRetryMaxWaitTime(config.RetryWait*10).
7673
SetHeader("User-Agent", config.UserAgent)
7774

78-
// Enable debug mode if requested
7975
if config.Debug {
8076
httpClient.SetDebug(true)
8177
}
8278

83-
// Add request middleware for logging and auth
8479
httpClient.AddRequestMiddleware(func(c *resty.Client, req *resty.Request) error {
85-
// Apply authentication
8680
if err := config.Auth.ApplyAuth(req); err != nil {
8781
return fmt.Errorf("auth failed: %w", err)
8882
}
8983

90-
// Log request
9184
config.Logger.Info("API request",
9285
zap.String("method", req.Method),
9386
zap.String("url", req.URL),
@@ -96,7 +89,6 @@ func NewClient(config Config) (*Client, error) {
9689
return nil
9790
})
9891

99-
// Add response middleware for logging and OAuth token refresh
10092
httpClient.AddResponseMiddleware(func(c *resty.Client, resp *resty.Response) error {
10193
config.Logger.Info("API response",
10294
zap.String("method", resp.Request.Method),
@@ -105,7 +97,6 @@ func NewClient(config Config) (*Client, error) {
10597
zap.String("status", resp.Status()),
10698
)
10799

108-
// Handle 401 responses for OAuth2Auth by forcing token refresh
109100
if resp.StatusCode() == 401 {
110101
if oauth2Auth, ok := config.Auth.(*OAuth2Auth); ok {
111102
config.Logger.Info("Received 401 response, forcing OAuth token refresh")

client/axm/client_builder.go

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,20 +73,6 @@ func (cb *ClientBuilder) WithJWTAuthFromEnv() *ClientBuilder {
7373
return cb.WithJWTAuthFromFile(keyID, issuerID, privateKeyPath)
7474
}
7575

76-
// WithAPIKeyAuth configures API key authentication
77-
func (cb *ClientBuilder) WithAPIKeyAuth(apiKey string) *ClientBuilder {
78-
// Note: API key auth would be implemented here when supported
79-
// For now, this is a placeholder for future implementation
80-
return cb
81-
}
82-
83-
// WithOAuth2Auth configures OAuth 2.0 authentication
84-
func (cb *ClientBuilder) WithOAuth2Auth(clientID, clientSecret string) *ClientBuilder {
85-
// Note: OAuth2 auth would be implemented here when supported
86-
// For now, this is a placeholder for future implementation
87-
return cb
88-
}
89-
9076
// WithBaseURL sets the base URL for the API (default: https://api-business.apple.com/v1)
9177
func (cb *ClientBuilder) WithBaseURL(baseURL string) *ClientBuilder {
9278
cb.baseURL = baseURL

client/axm/constants.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package axm
2+
3+
// OAuth scope constants
4+
const (
5+
ScopeBusinessAPI = "business.api"
6+
ScopeSchoolAPI = "school.api"
7+
)
8+
9+
// Default OAuth endpoints
10+
const (
11+
DefaultOAuthTokenURL = "https://account.apple.com/auth/oauth2/token"
12+
DefaultOAuthAudience = "https://account.apple.com/auth/oauth2/v2/token"
13+
)

client/axm/errors.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,7 @@ func NewErrorHandler(logger *zap.Logger) *ErrorHandler {
116116
func (eh *ErrorHandler) HandleError(resp *resty.Response, errorResp *ErrorResponse) error {
117117
statusCode := resp.StatusCode()
118118

119-
// Handle new ErrorResponse format with errors array
120119
if len(errorResp.Errors) > 0 {
121-
// Log all errors with enhanced information
122120
for i, apiError := range errorResp.Errors {
123121
logFields := []zap.Field{
124122
zap.Int("error_index", i),
@@ -131,7 +129,6 @@ func (eh *ErrorHandler) HandleError(resp *resty.Response, errorResp *ErrorRespon
131129
zap.String("method", resp.Request.Method),
132130
}
133131

134-
// Add source information if available
135132
if apiError.Source != nil {
136133
if apiError.Source.JsonPointer != nil {
137134
logFields = append(logFields, zap.String("source_json_pointer", apiError.Source.JsonPointer.Pointer))
@@ -141,7 +138,6 @@ func (eh *ErrorHandler) HandleError(resp *resty.Response, errorResp *ErrorRespon
141138
}
142139
}
143140

144-
// Add links information if available
145141
if apiError.Links != nil {
146142
if apiError.Links.About != "" {
147143
logFields = append(logFields, zap.String("links_about", apiError.Links.About))
@@ -154,28 +150,24 @@ func (eh *ErrorHandler) HandleError(resp *resty.Response, errorResp *ErrorRespon
154150
}
155151
}
156152

157-
// Add meta information if available
158153
if apiError.Meta != nil && apiError.Meta.AdditionalProperties != nil {
159154
logFields = append(logFields, zap.Any("error_meta", apiError.Meta.AdditionalProperties))
160155
}
161156

162157
eh.logger.Error("API request failed", logFields...)
163158
}
164159

165-
// Return the first error
166160
firstError := errorResp.Errors[0]
167161
return &firstError
168162
}
169163

170-
// Fallback for when no structured errors are provided
171164
eh.logger.Error("API request failed (no structured error)",
172165
zap.Int("status_code", statusCode),
173166
zap.String("url", resp.Request.URL),
174167
zap.String("method", resp.Request.Method),
175168
zap.String("response_body", resp.String()),
176169
)
177170

178-
// Return generic error
179171
return &APIError{
180172
Status: fmt.Sprintf("%d", statusCode),
181173
Code: fmt.Sprintf("HTTP_%d", statusCode),

client/axm/pagination.go

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"maps"
78
"net/url"
89
)
910

@@ -56,13 +57,11 @@ func (c *Client) GetNextPage(ctx context.Context, nextURL string, headers map[st
5657
return fmt.Errorf("no next page URL provided")
5758
}
5859

59-
// Parse the next URL to extract path and query parameters
6060
parsedURL, err := url.Parse(nextURL)
6161
if err != nil {
6262
return fmt.Errorf("failed to parse next URL: %w", err)
6363
}
6464

65-
// Extract query parameters
6665
queryParams := make(map[string]string)
6766
for key, values := range parsedURL.Query() {
6867
if len(values) > 0 {
@@ -87,46 +86,36 @@ func HasPrevPage(links *Links) bool {
8786
// GetAllPages retrieves all pages of results by following pagination links
8887
func (c *Client) GetAllPages(ctx context.Context, path string, queryParams map[string]string, headers map[string]string, processPage func([]byte) error) error {
8988
currentParams := make(map[string]string)
90-
for k, v := range queryParams {
91-
currentParams[k] = v
92-
}
89+
maps.Copy(currentParams, queryParams)
9390

9491
for {
95-
// Make request for current page
9692
var rawResponse json.RawMessage
9793
err := c.GetPaginated(ctx, path, currentParams, headers, &rawResponse)
9894
if err != nil {
9995
return err
10096
}
10197

102-
// Process the page data
10398
if err := processPage(rawResponse); err != nil {
10499
return err
105100
}
106101

107-
// Parse response to check for next page
108102
var pageInfo struct {
109103
Links *Links `json:"links,omitempty"`
110104
}
111105
if err := json.Unmarshal(rawResponse, &pageInfo); err != nil {
112106
return fmt.Errorf("failed to parse pagination info: %w", err)
113107
}
114108

115-
// Check if there's a next page
116109
if !HasNextPage(pageInfo.Links) {
117110
break
118111
}
119112

120-
// Extract parameters from next URL
121113
nextParams, err := extractParamsFromURL(pageInfo.Links.Next)
122114
if err != nil {
123115
return fmt.Errorf("failed to parse next URL: %w", err)
124116
}
125117

126-
// Update parameters for next request
127-
for k, v := range nextParams {
128-
currentParams[k] = v
129-
}
118+
maps.Copy(currentParams, nextParams)
130119
}
131120

132121
return nil

0 commit comments

Comments
 (0)