blob: bdc9d8bd254bb3d9889abb814364f1a29f09e547 [file] [log] [blame]
Akron90f65212025-06-12 14:32:55 +02001package service
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "io"
8 "net/http"
9 "net/url"
10 "strings"
11 "time"
12
13 "github.com/korap/korap-mcp/auth"
14 "github.com/korap/korap-mcp/config"
15)
16
17// Client represents a KorAP API client
18type Client struct {
19 baseURL string
20 httpClient *http.Client
21 oauthClient *auth.OAuthClient
22}
23
24// ClientOptions configures the KorAP client
25type ClientOptions struct {
26 BaseURL string
27 Timeout time.Duration
28 OAuthConfig *config.OAuthConfig
29}
30
31// NewClient creates a new KorAP API client
32func NewClient(opts ClientOptions) (*Client, error) {
33 if opts.BaseURL == "" {
34 return nil, fmt.Errorf("base URL is required")
35 }
36
37 // Ensure base URL ends with /
38 baseURL := strings.TrimSuffix(opts.BaseURL, "/") + "/"
39
40 // Set default timeout if not specified
41 timeout := opts.Timeout
42 if timeout == 0 {
43 timeout = 30 * time.Second
44 }
45
46 client := &Client{
47 baseURL: baseURL,
48 httpClient: &http.Client{
49 Timeout: timeout,
50 },
51 }
52
53 // Initialize OAuth client if configuration is provided
54 if opts.OAuthConfig != nil && opts.OAuthConfig.Enabled {
55 oauthClient, err := auth.NewOAuthClient(opts.OAuthConfig)
56 if err != nil {
57 return nil, fmt.Errorf("failed to create OAuth client: %w", err)
58 }
59 client.oauthClient = oauthClient
60 }
61
62 return client, nil
63}
64
65// SetOAuthClient sets the OAuth client for authentication
66func (c *Client) SetOAuthClient(oauthClient *auth.OAuthClient) {
67 c.oauthClient = oauthClient
68}
69
70// IsAuthenticated returns true if the client has valid authentication
71func (c *Client) IsAuthenticated() bool {
72 return c.oauthClient != nil && c.oauthClient.IsAuthenticated()
73}
74
75// AuthenticateWithClientCredentials performs client credentials OAuth flow
76func (c *Client) AuthenticateWithClientCredentials(ctx context.Context) error {
77 if c.oauthClient == nil {
78 return fmt.Errorf("OAuth client not configured")
79 }
80
81 return c.oauthClient.ClientCredentialsFlow(ctx)
82}
83
84// buildURL constructs a full URL from the base URL and endpoint
85func (c *Client) buildURL(endpoint string) (string, error) {
86 endpoint = strings.TrimPrefix(endpoint, "/")
87 if endpoint == "" {
88 return c.baseURL, nil
89 }
90 fullURL, err := url.JoinPath(c.baseURL, endpoint)
91 if err != nil {
92 return "", fmt.Errorf("failed to build URL: %w", err)
93 }
94 return fullURL, nil
95}
96
97// doRequest performs an HTTP request with optional authentication
98func (c *Client) doRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
99 fullURL, err := c.buildURL(endpoint)
100 if err != nil {
101 return nil, err
102 }
103
104 req, err := http.NewRequestWithContext(ctx, method, fullURL, body)
105 if err != nil {
106 return nil, fmt.Errorf("failed to create request: %w", err)
107 }
108
109 // Set common headers
110 req.Header.Set("Accept", "application/json")
111 if body != nil {
112 req.Header.Set("Content-Type", "application/json")
113 }
114
115 // Add authentication if available
116 if c.oauthClient != nil && c.oauthClient.IsAuthenticated() {
117 if err := c.oauthClient.AddAuthHeader(req); err != nil {
118 return nil, fmt.Errorf("failed to add auth header: %w", err)
119 }
120 }
121
122 // Use OAuth HTTP client if available, otherwise use default client
123 httpClient := c.httpClient
124 if c.oauthClient != nil {
125 httpClient = c.oauthClient.GetHTTPClient()
126 }
127
128 resp, err := httpClient.Do(req)
129 if err != nil {
130 return nil, fmt.Errorf("request failed: %w", err)
131 }
132
133 return resp, nil
134}
135
136// Get performs a GET request to the specified endpoint
137func (c *Client) Get(ctx context.Context, endpoint string) (*http.Response, error) {
138 return c.doRequest(ctx, http.MethodGet, endpoint, nil)
139}
140
141// Post performs a POST request to the specified endpoint with JSON body
142func (c *Client) Post(ctx context.Context, endpoint string, body interface{}) (*http.Response, error) {
143 var bodyReader io.Reader
144 if body != nil {
145 jsonBody, err := json.Marshal(body)
146 if err != nil {
147 return nil, fmt.Errorf("failed to marshal request body: %w", err)
148 }
149 bodyReader = strings.NewReader(string(jsonBody))
150 }
151
152 return c.doRequest(ctx, http.MethodPost, endpoint, bodyReader)
153}
154
155// GetJSON performs a GET request and unmarshals the JSON response
156func (c *Client) GetJSON(ctx context.Context, endpoint string, target interface{}) error {
157 resp, err := c.Get(ctx, endpoint)
158 if err != nil {
159 return err
160 }
161 defer resp.Body.Close()
162
163 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
164 return c.handleErrorResponse(resp)
165 }
166
167 if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
168 return fmt.Errorf("failed to decode JSON response: %w", err)
169 }
170
171 return nil
172}
173
174// PostJSON performs a POST request and unmarshals the JSON response
175func (c *Client) PostJSON(ctx context.Context, endpoint string, body interface{}, target interface{}) error {
176 resp, err := c.Post(ctx, endpoint, body)
177 if err != nil {
178 return err
179 }
180 defer resp.Body.Close()
181
182 if resp.StatusCode < 200 || resp.StatusCode >= 300 {
183 return c.handleErrorResponse(resp)
184 }
185
186 if target != nil {
187 if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
188 return fmt.Errorf("failed to decode JSON response: %w", err)
189 }
190 }
191
192 return nil
193}
194
195// handleErrorResponse processes error responses from the KorAP API
196func (c *Client) handleErrorResponse(resp *http.Response) error {
197 body, err := io.ReadAll(resp.Body)
198 if err != nil {
199 return fmt.Errorf("HTTP %d: failed to read error response", resp.StatusCode)
200 }
201
202 // Try to parse as KorAP error response
203 var errorResp ErrorResponse
204 if json.Unmarshal(body, &errorResp) == nil && errorResp.Error != "" {
205 return &APIError{
206 StatusCode: resp.StatusCode,
207 Message: errorResp.Error,
208 Details: errorResp.ErrorDescription,
209 }
210 }
211
212 // Fallback to generic error
213 return &APIError{
214 StatusCode: resp.StatusCode,
215 Message: fmt.Sprintf("HTTP %d", resp.StatusCode),
216 Details: string(body),
217 }
218}
219
220// GetBaseURL returns the base URL of the KorAP instance
221func (c *Client) GetBaseURL() string {
222 return c.baseURL
223}
224
225// Ping checks if the KorAP server is reachable
226func (c *Client) Ping(ctx context.Context) error {
227 resp, err := c.Get(ctx, "")
228 if err != nil {
229 return fmt.Errorf("failed to ping KorAP server: %w", err)
230 }
231 defer resp.Body.Close()
232
233 if resp.StatusCode >= 500 {
234 return fmt.Errorf("KorAP server error: HTTP %d", resp.StatusCode)
235 }
236
237 return nil
238}