crud.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package lunatask
  6
  7import (
  8	"context"
  9	"fmt"
 10	"net/http"
 11	"net/url"
 12)
 13
 14// unwrap extracts an entity from a response wrapper.
 15type unwrap[E, R any] func(R) E
 16
 17// unwrapSlice extracts a slice of entities from a response wrapper.
 18type unwrapSlice[E, R any] func(R) []E
 19
 20// SourceFilter provides optional source filtering for list operations.
 21type SourceFilter interface {
 22	GetSource() *string
 23	GetSourceID() *string
 24}
 25
 26// get fetches a single entity by ID.
 27func get[E, R any](ctx context.Context, c *Client, path, id, name string, extract unwrap[E, R]) (*E, error) {
 28	if id == "" {
 29		return nil, fmt.Errorf("%w: %s ID cannot be empty", ErrBadRequest, name)
 30	}
 31
 32	resp, _, err := doJSON[R](ctx, c, http.MethodGet, path+"/"+id, nil)
 33	if err != nil {
 34		return nil, err
 35	}
 36
 37	e := extract(*resp)
 38
 39	return &e, nil
 40}
 41
 42// list fetches all entities, optionally filtered by source.
 43func list[E, R any](
 44	ctx context.Context, c *Client, path string, opts SourceFilter, extract unwrapSlice[E, R],
 45) ([]E, error) {
 46	if opts != nil {
 47		params := url.Values{}
 48
 49		if s := opts.GetSource(); s != nil && *s != "" {
 50			params.Set("source", *s)
 51		}
 52
 53		if s := opts.GetSourceID(); s != nil && *s != "" {
 54			params.Set("source_id", *s)
 55		}
 56
 57		if len(params) > 0 {
 58			path = fmt.Sprintf("%s?%s", path, params.Encode())
 59		}
 60	}
 61
 62	resp, _, err := doJSON[R](ctx, c, http.MethodGet, path, nil)
 63	if err != nil {
 64		return nil, err
 65	}
 66
 67	return extract(*resp), nil
 68}
 69
 70// create adds a new entity. Returns (nil, nil) if duplicate exists (HTTP 204).
 71func create[E, R any](ctx context.Context, c *Client, path string, body any, extract unwrap[E, R]) (*E, error) {
 72	resp, noContent, err := doJSON[R](ctx, c, http.MethodPost, path, body)
 73	if err != nil {
 74		return nil, err
 75	}
 76
 77	if noContent {
 78		return nil, nil //nolint:nilnil
 79	}
 80
 81	e := extract(*resp)
 82
 83	return &e, nil
 84}
 85
 86// update modifies an entity by ID.
 87func update[E, R any](
 88	ctx context.Context, c *Client, path, id, name string, body any, extract unwrap[E, R],
 89) (*E, error) {
 90	if id == "" {
 91		return nil, fmt.Errorf("%w: %s ID cannot be empty", ErrBadRequest, name)
 92	}
 93
 94	resp, _, err := doJSON[R](ctx, c, http.MethodPut, path+"/"+id, body)
 95	if err != nil {
 96		return nil, err
 97	}
 98
 99	e := extract(*resp)
100
101	return &e, nil
102}
103
104// del removes an entity by ID and returns its final state.
105func del[E, R any](ctx context.Context, c *Client, path, id, name string, extract unwrap[E, R]) (*E, error) {
106	if id == "" {
107		return nil, fmt.Errorf("%w: %s ID cannot be empty", ErrBadRequest, name)
108	}
109
110	resp, _, err := doJSON[R](ctx, c, http.MethodDelete, path+"/"+id, nil)
111	if err != nil {
112		return nil, err
113	}
114
115	e := extract(*resp)
116
117	return &e, nil
118}