packages feed

todoist-sdk-0.1.2.1: README.md

# TodoistSDK

[![Hackage](https://img.shields.io/hackage/v/todoist-sdk.svg)](https://hackage.haskell.org/package/todoist-sdk)

An unofficial Haskell SDK for the [Todoist REST API](https://developer.todoist.com/rest/v2/). Manage projects, tasks, comments, sections, and labels with type-safe, ergonomic Haskell functions.

## Features

- **Complete API Coverage**: Projects, Tasks, Comments, Sections, and Labels
- **Type-Safe**: Leverages Haskell's type system to prevent common errors
- **Getter-Only Lenses**: Read-only lenses for convenient field access and composition
- **Ergonomic Builder Pattern**: Easily construct API requests with optional fields
- **Automatic Pagination**: Transparently fetches all pages for list operations
- **Flexible Error Handling**: Operations return `Either TodoistError a` for explicit error handling
- **Testing Support (In Progress)**: Includes Trace interpreter for testing without API calls

## Installation

### Using Stack

Add to your `stack.yaml`:

```yaml
extra-deps:
  - todoist-sdk-0.1.2.1
```

Add to your `package.yaml` or `.cabal` file:

```yaml
dependencies:
  - todoist-sdk
```

### Using Cabal

Add to your `.cabal` file's `build-depends`:

```cabal
build-depends:
    base >=4.7 && <5
  , todoist-sdk
```

Then install:

```bash
cabal update
cabal install todoist-sdk
```

## Quick Start

Get your API token from [Todoist Settings → Integrations → Developer](https://todoist.com/prefs/integrations).

### Try it in the REPL

```bash
$ stack repl
```

```haskell
>>> import Web.Todoist
>>> let config = newTodoistConfig "your-api-token-here"
>>> result <- todoist config getAllProjects
>>> case result of
      Left err -> print err
      Right projects -> mapM_ print projects
```

### Complete Example

Create a file `example.hs`:

```haskell
#!/usr/bin/env stack
-- stack --resolver lts-24.7 script --package todoist-sdk --package text

{-# LANGUAGE OverloadedStrings #-}

import Web.Todoist

main :: IO ()
main = do
    -- Configure with your API token
    let config = newTodoistConfig "your-api-token-here"

    result <- todoist config $ do
        -- Create a new project
        let newProj = runBuilder (createProjectBuilder "My Haskell Project")
                      (withDescription "Learning Haskell SDK" <> withViewStyle Board)
        project <- addProject newProj

        -- Create a task in the project
        let newTask = runBuilder (newTaskBuilder "Read documentation")
                      (withProjectId (_id project) <> withPriority 2)
        task <- createTask newTask

        -- Get all tasks
        tasks <- getTasks taskParamBuilder

        pure (project, task, tasks)

    case result of
        Left err -> putStrLn $ "Error: " ++ show err
        Right (proj, task, tasks) -> do
            putStrLn $ "Created project: " ++ show (_name proj)
            putStrLn $ "Created task: " ++ show (_content task)
            putStrLn $ "Total tasks: " ++ show (length tasks)
```

Run it:

```bash
chmod +x example.hs
./example.hs
```

## Common Usage Examples

### Working with Projects

```haskell
import Web.Todoist

let config = newTodoistConfig "your-api-token"

-- Get all projects
result <- todoist config getAllProjects

-- Create a project with optional fields
let newProject = runBuilder (createProjectBuilder "Shopping List")
                 (withColor "blue" <> withViewStyle List <> withIsFavorite True)
project <- todoist config (addProject createProjectBuilder)

-- Update a project
let update = runBuilder emptyProjectUpdate (withName "Updated Name")
updated <- todoist config (updateProject update projectId)

-- Delete a project
todoist config (deleteProject projectId)
```

### Working with Tasks

```haskell
-- Create a task with due date
let task = runBuilder (newTaskBuilder "Buy milk")
           (withProjectId "project-123"
            <> withDueString "tomorrow"
            <> withPriority 3
            <> withLabels ["grocery", "urgent"])
result <- todoist config (createTask task)

-- Get tasks with filters
let params = TaskParam
    { project_id = Just "project-123"
    , filter = Nothing
    , label_id = Nothing
    , cursor = Nothing
    , limit = Nothing
    }
tasks <- todoist config (getTasks params)

-- Complete a task
todoist config (closeTask taskId)

-- Update a task
let update = runBuilder updateTaskBuilder (withContent "Buy 2% milk")
updated <- todoist config (updateTask update taskId)
```

### Working with Comments

```haskell
-- Add a comment to a task
let comment = runBuilder (newCommentBuilder "Don't forget organic!")
              (withTaskId "task-456")
result <- todoist config (addComment comment)

-- Get all comments for a project
let params = CommentParam
    { project_id = Just "project-123"
    , task_id = Nothing
    , cursor = Nothing
    , limit = Nothing
    , public_key = Nothing
    }
comments <- todoist config (getComments params)
```

### Working with Sections

```haskell
-- Create a section
let section = runBuilder (newSection "In Progress" "project-123") mempty
result <- todoist config (addSection section)

-- Get sections for a project with builder pattern
let params = runBuilder newSectionParam (withProjectId "project-123" <> withLimit 50)
sections <- todoist config (getSections params)
```

### Working with Labels

```haskell
-- Create a label
let label = runBuilder (createLabelBuilder "urgent") mempty
result <- todoist config (addLabel label)

-- Get all labels
let params = runBuilder labelParamBuilder (withLimit 50)
labels <- todoist config (getLabels params)
```

## Working with Lenses

TodoistSDK provides **getter-only lenses** for all domain types, enabling convenient field access and composition. All lenses are read-only; for mutations, use the builder pattern.

### Basic Usage

```haskell
import Web.Todoist
import Web.Todoist.Lens ((^.))  -- Import the getter operator

-- Get all projects
Right projects <- todoist config getAllProjects

-- Extract fields using lenses
let firstProject = head projects
let projectName = firstProject ^. name
let projectColor = firstProject ^. color
let isShared = firstProject ^. isShared

print projectName  -- Prints: Name "My Project"
```

### Composing Lenses

Lenses can be composed to access nested fields:

```haskell
-- Get tasks
Right tasks <- todoist config (getTasks taskParamBuilder)

-- Access nested fields
let task = head tasks
let maybeDueDate = task ^. taskDue >>= (^. dueDate)
let taskContent = task ^. taskContent

-- Check if task has a deadline
case task ^. taskDeadline of
    Just deadline -> putStrLn $ "Deadline: " ++ show (deadline ^. deadlineDate)
    Nothing -> putStrLn "No deadline set"
```

### Available Lenses

All domain types provide lenses for their fields:

**Project**: `projectId`, `name`, `description`, `order`, `color`, `isCollapsed`, `isShared`, `isFavorite`, `isArchived`, `canAssignTasks`, `viewStyle`, `createdAt`, `updatedAt`

**Task**: `taskId`, `taskContent`, `taskDescription`, `taskProjectId`, `taskSectionId`, `taskParentId`, `taskLabels`, `taskPriority`, `taskDue`, `taskDeadline`, `taskDuration`, `taskIsCollapsed`, `taskOrder`, `taskAssigneeId`, `taskAssignerId`, `taskCompletedAt`, `taskCreatorId`, `taskCreatedAt`, `taskUpdatedAt`

**Comment**: `commentId`, `commentContent`, `commentPosterId`, `commentPostedAt`, `commentTaskId`, `commentProjectId`, `commentAttachment`

**Section**: `sectionId`, `sectionName`, `sectionProjectId`, `sectionIsCollapsed`, `sectionOrder`

**Label**: `labelId`, `labelName`, `labelColor`, `labelOrder`, `labelIsFavorite`

**Due**: `dueDate`, `dueString`, `dueLang`, `dueIsRecurring`, `dueTimezone`

**Deadline**: `deadlineDate`, `deadlineLang`

**Duration**: `amount`, `unit`

### Mutation with Builders

Lenses are **read-only**. For field mutations, use the builder pattern:

```haskell
-- Reading with lenses
let oldName = project ^. name

-- Updating with builders (not lenses)
let update = runBuilder updateProjectBuilder (withName "New Name")
Right updatedProject <- todoist config (updateProject update projectId)
```

## Error Handling

All operations return `Either TodoistError a`. The `TodoistError` type includes:

- `BadRequest` - Invalid request parameters
- `Unauthorized` - Invalid or missing API token
- `Forbidden` - Insufficient permissions
- `NotFound` - Resource doesn't exist
- `HttpError String` - Other HTTP errors

Example:

```haskell
result <- todoist config (getProject projectId)
case result of
    Left BadRequest -> putStrLn "Invalid project ID"
    Left Unauthorized -> putStrLn "Check your API token"
    Left NotFound -> putStrLn "Project not found"
    Left (HttpError msg) -> putStrLn $ "HTTP error: " ++ msg
    Right project -> print project
```

## Documentation

Full API documentation is available on [Hackage](https://hackage.haskell.org/package/todoist-sdk).

For details on the Todoist REST API, see the [official documentation](https://developer.todoist.com/rest/v2/).

## License

This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## Acknowledgments

This library is a labor of love to the Haskell community and to the Todoist app. It is an unofficial SDK and is not affiliated with or endorsed by Doist.