shared.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5// Package shared provides common interfaces, types, and helpers for MCP tools.
  6package shared
  7
  8import (
  9	"errors"
 10	"fmt"
 11	"time"
 12)
 13
 14// ErrTimezoneNotConfigured is returned when the timezone config value is empty.
 15var ErrTimezoneNotConfigured = errors.New(
 16	"timezone is not configured; please set the 'timezone' value in your " +
 17		"config file (e.g. 'UTC' or 'America/New_York')",
 18)
 19
 20// AreaProvider defines the interface for accessing area data.
 21type AreaProvider interface {
 22	GetName() string
 23	GetID() string
 24	GetKey() string
 25	GetGoals() []GoalProvider
 26}
 27
 28// GoalProvider defines the interface for accessing goal data.
 29//
 30//nolint:iface // semantically distinct from HabitProvider
 31type GoalProvider interface {
 32	GetName() string
 33	GetID() string
 34	GetKey() string
 35}
 36
 37// HabitProvider defines the interface for accessing habit data.
 38//
 39//nolint:iface // semantically distinct from GoalProvider
 40type HabitProvider interface {
 41	GetName() string
 42	GetID() string
 43	GetKey() string
 44}
 45
 46// Config holds the necessary configuration for tool handlers.
 47type Config struct {
 48	AccessToken string
 49	Timezone    string
 50	Areas       []AreaProvider
 51	Habits      []HabitProvider
 52}
 53
 54// LoadLocation loads a timezone location string, returning a *time.Location or error.
 55func LoadLocation(timezone string) (*time.Location, error) {
 56	if timezone == "" {
 57		return nil, ErrTimezoneNotConfigured
 58	}
 59
 60	loc, err := time.LoadLocation(timezone)
 61	if err != nil {
 62		return nil, fmt.Errorf("could not load timezone '%s': %w", timezone, err)
 63	}
 64
 65	return loc, nil
 66}
 67
 68// FindArea finds an area by ID or key from the list of providers. Returns nil if not found.
 69func FindArea(areas []AreaProvider, idOrKey string) AreaProvider {
 70	for _, ap := range areas {
 71		if ap.GetID() == idOrKey || ap.GetKey() == idOrKey {
 72			return ap
 73		}
 74	}
 75
 76	return nil
 77}
 78
 79// FindGoalInArea checks if a goal exists within an area by ID or key.
 80// Returns true if found.
 81func FindGoalInArea(area AreaProvider, idOrKey string) bool {
 82	for _, goal := range area.GetGoals() {
 83		if goal.GetID() == idOrKey || goal.GetKey() == idOrKey {
 84			return true
 85		}
 86	}
 87
 88	return false
 89}
 90
 91// GetGoalInArea finds a goal within an area by ID or key.
 92// Returns nil if not found.
 93func GetGoalInArea(area AreaProvider, idOrKey string) GoalProvider {
 94	for _, goal := range area.GetGoals() {
 95		if goal.GetID() == idOrKey || goal.GetKey() == idOrKey {
 96			return goal
 97		}
 98	}
 99
100	return nil
101}
102
103// FindHabit finds a habit by ID or key from the list of providers. Returns nil if not found.
104func FindHabit(habits []HabitProvider, idOrKey string) HabitProvider {
105	for _, hp := range habits {
106		if hp.GetID() == idOrKey || hp.GetKey() == idOrKey {
107			return hp
108		}
109	}
110
111	return nil
112}