package findatender import ( "context" "log/slog" "time" resty "resty.dev/v3" "git.avsdev.uk/Go/job-search/internal/types" ) // FindATender is a client for the Find a Tender OCDS API (or the mock // server). Create it with New and call Shutdown when finished with it. type FindATender struct { httpClient *resty.Client apiRoot string ctx context.Context log *slog.Logger } // Option configures a FindATender client. type Option func(*FindATender) // WithLogger makes the client log to logger instead of slog.Default(). func WithLogger(logger *slog.Logger) Option { return func(ft *FindATender) { ft.log = logger } } // New returns a client for the OCDS API at apiRoot. Requests time out after // 60 seconds and are retried once. Call Shutdown when finished with it. It // logs to slog.Default() unless given WithLogger. func New(apiRoot string, opts ...Option) *FindATender { findATender := &FindATender{ apiRoot: apiRoot, } for _, opt := range opts { opt(findATender) } findATender.httpClient = resty.New(). SetTimeout(60 * time.Second). SetRetryCount(1) return findATender } // logger returns the logger the client logs to, tagged with its component. func (ft *FindATender) logger() *slog.Logger { log := ft.log if log == nil { log = slog.Default() } return log.With("component", "findatender") } // With returns a copy of the client whose requests use ctx, so they stop // when it is cancelled. A nil ctx returns the client unchanged. func (ft *FindATender) With(ctx context.Context) *FindATender { if ctx == nil { return ft } copied := *ft copied.ctx = ctx return &copied } // Shutdown closes the client's HTTP connections. A failure to close is // logged, as there is nothing a caller could do about it. func (ft *FindATender) Shutdown() { if err := ft.httpClient.Close(); err != nil { ft.logger().Warn("closing http client failed", "error", err) } } // FetchOnce fetches the first page of releases updated since since, and // converts them to opportunities. If allowRateLimit is true, a 429 response // is waited out and retried; otherwise it fails with ErrRateLimitExceeded. func (ft *FindATender) FetchOnce( since time.Time, allowRateLimit bool, ) ([]*types.Opportunity, error) { releases, err := ft.fetchOnce(RequestOptions{ Since: since, AllowRateLimit: allowRateLimit, }) if err != nil { return nil, err } return Convert(releases), nil } // FetchAll fetches every page of releases updated since since, following // each page's next link, and converts them to opportunities. allowRateLimit // behaves as for FetchOnce. func (ft *FindATender) FetchAll( since time.Time, allowRateLimit bool, ) ([]*types.Opportunity, error) { releases, err := ft.fetchAll(RequestOptions{ Since: since, AllowRateLimit: allowRateLimit, }) if err != nil { return nil, err } return Convert(releases), nil }