# TodoistSDK
[](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.