homelab/services/parts-inventory/cli/internal/client/client.go
Dan V deb6c38d7b chore: commit homelab setup — deployment, services, orchestration, skill
- Add .gitignore: exclude compiled binaries, build artifacts, and Helm
  values files containing real secrets (authentik, prometheus)
- Add all Kubernetes deployment manifests (deployment/)
- Add services source code: ha-sync, device-inventory, games-console,
  paperclip, parts-inventory
- Add Ansible orchestration: playbooks, roles, inventory, cloud-init
- Add hardware specs, execution plans, scripts, HOMELAB.md
- Add skills/homelab/SKILL.md + skills/install.sh to preserve Copilot skill
- Remove previously-tracked inventory-cli binary from git index

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 08:10:32 +02:00

205 lines
5.1 KiB
Go

package client
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
// Part mirrors the API's IPart document.
type Part struct {
ID string `json:"_id"`
Title string `json:"title"`
Type string `json:"type"`
Category string `json:"category"`
Manufacturer string `json:"manufacturer,omitempty"`
Dimensions *Dimensions `json:"dimensions,omitempty"`
Quantity int `json:"quantity"`
Location string `json:"location,omitempty"`
Notes string `json:"notes,omitempty"`
Tags []string `json:"tags"`
Properties map[string]interface{} `json:"properties"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// Dimensions holds optional physical size info.
type Dimensions struct {
Width *float64 `json:"width,omitempty"`
Length *float64 `json:"length,omitempty"`
Height *float64 `json:"height,omitempty"`
Unit string `json:"unit,omitempty"`
}
// ListResponse is the shape returned by GET /api/parts.
type ListResponse struct {
Total int `json:"total"`
Parts []Part `json:"parts"`
}
// Client wraps HTTP calls to the parts API.
type Client struct {
BaseURL string
HTTPClient *http.Client
}
// New creates a Client with a 15-second timeout.
func New(baseURL string) *Client {
return &Client{
BaseURL: baseURL,
HTTPClient: &http.Client{Timeout: 15 * time.Second},
}
}
// ListParts fetches parts with optional full-text query and field filters.
func (c *Client) ListParts(query, partType, category string) (*ListResponse, error) {
u, err := url.Parse(c.BaseURL + "/api/parts")
if err != nil {
return nil, err
}
params := url.Values{}
if query != "" {
params.Set("q", query)
}
if partType != "" {
params.Set("type", partType)
}
if category != "" {
params.Set("category", category)
}
u.RawQuery = params.Encode()
resp, err := c.HTTPClient.Get(u.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned %d", resp.StatusCode)
}
var result ListResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
return &result, nil
}
// GetPart fetches a single part by ID.
func (c *Client) GetPart(id string) (*Part, error) {
resp, err := c.HTTPClient.Get(c.BaseURL + "/api/parts/" + id)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("part %s not found", id)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned %d", resp.StatusCode)
}
var part Part
if err := json.NewDecoder(resp.Body).Decode(&part); err != nil {
return nil, err
}
return &part, nil
}
// CreatePart posts a new part to the API.
func (c *Client) CreatePart(part map[string]interface{}) (*Part, error) {
body, err := json.Marshal(part)
if err != nil {
return nil, err
}
resp, err := c.HTTPClient.Post(
c.BaseURL+"/api/parts",
"application/json",
bytes.NewReader(body),
)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
var apiErr map[string]string
_ = json.NewDecoder(resp.Body).Decode(&apiErr)
if msg, ok := apiErr["error"]; ok {
return nil, fmt.Errorf("API error: %s", msg)
}
return nil, fmt.Errorf("API returned %d", resp.StatusCode)
}
var created Part
if err := json.NewDecoder(resp.Body).Decode(&created); err != nil {
return nil, err
}
return &created, nil
}
// UpdatePart sends a PUT request with the provided fields.
func (c *Client) UpdatePart(id string, updates map[string]interface{}) (*Part, error) {
body, err := json.Marshal(updates)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodPut, c.BaseURL+"/api/parts/"+id, bytes.NewReader(body))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("part %s not found", id)
}
if resp.StatusCode != http.StatusOK {
var apiErr map[string]string
_ = json.NewDecoder(resp.Body).Decode(&apiErr)
if msg, ok := apiErr["error"]; ok {
return nil, fmt.Errorf("API error: %s", msg)
}
return nil, fmt.Errorf("API returned %d", resp.StatusCode)
}
var updated Part
if err := json.NewDecoder(resp.Body).Decode(&updated); err != nil {
return nil, err
}
return &updated, nil
}
// DeletePart sends a DELETE request for the given part ID.
func (c *Client) DeletePart(id string) error {
req, err := http.NewRequest(http.MethodDelete, c.BaseURL+"/api/parts/"+id, nil)
if err != nil {
return err
}
resp, err := c.HTTPClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("part %s not found", id)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("API returned %d", resp.StatusCode)
}
return nil
}