feat(bookstack-api-dd0): implement ChaptersService List, Get, ListAll
Implement ChaptersService with List, Get, and ListAll iterator. Add mock server tests.
This commit is contained in:
parent
f2747adcaf
commit
ac29420b5f
4 changed files with 89 additions and 33 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"worktree_root": "/home/oli/Dev/bookstack-api",
|
"worktree_root": "/home/oli/Dev/bookstack-api",
|
||||||
"last_export_commit": "1a26e9035a88904a838791e0519e57fc72c0d4b1",
|
"last_export_commit": "6944d50512deb03d2c3bceca820b6860c4642dfb",
|
||||||
"last_export_time": "2026-01-30T09:51:35.620687129+01:00",
|
"last_export_time": "2026-01-30T09:52:10.984477534+01:00",
|
||||||
"jsonl_hash": "37af0523d6844b26609601b8a93df2706f3ca73077eaee918a4df24581c75793"
|
"jsonl_hash": "2783bc5e575c6cce1ab9a6e3c52a4da30f062d285956a442d248eca884228cff"
|
||||||
}
|
}
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
{"id":"bookstack-api-bu8","title":"Add GoDoc documentation for all public APIs","description":"Ensure all exported types and functions have proper GoDoc comments.\n\n## Requirements\nFrom PRD Section 8 (Definition of Done):\n- Alle public APIs dokumentiert (GoDoc)\n\n## Documentation Standards\n- Package-level doc comment in bookstack.go\n- All exported types documented\n- All exported functions documented\n- All exported constants/variables documented\n- Include usage examples in doc comments where helpful\n\n## Example\n```go\n// Client is the Bookstack API client. Use NewClient to create a new instance.\n// Client is safe for concurrent use.\ntype Client struct {\n // Books provides access to the Books API.\n Books *BooksService\n ...\n}\n\n// NewClient creates a new Bookstack API client with the provided configuration.\n// BaseURL, TokenID, and TokenSecret are required.\n//\n// Example:\n//\n// client := bookstack.NewClient(bookstack.Config{\n// BaseURL: \"https://docs.example.com\",\n// TokenID: \"token-id\",\n// TokenSecret: \"token-secret\",\n// })\nfunc NewClient(cfg Config) *Client\n```\n\n## Acceptance Criteria\n- [ ] Package documentation present\n- [ ] All exported types documented\n- [ ] All exported functions documented\n- [ ] Documentation renders correctly in pkg.go.dev style\n- [ ] No golint documentation warnings","status":"open","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:40:24.752365504+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:40:24.752365504+01:00"}
|
{"id":"bookstack-api-bu8","title":"Add GoDoc documentation for all public APIs","description":"Ensure all exported types and functions have proper GoDoc comments.\n\n## Requirements\nFrom PRD Section 8 (Definition of Done):\n- Alle public APIs dokumentiert (GoDoc)\n\n## Documentation Standards\n- Package-level doc comment in bookstack.go\n- All exported types documented\n- All exported functions documented\n- All exported constants/variables documented\n- Include usage examples in doc comments where helpful\n\n## Example\n```go\n// Client is the Bookstack API client. Use NewClient to create a new instance.\n// Client is safe for concurrent use.\ntype Client struct {\n // Books provides access to the Books API.\n Books *BooksService\n ...\n}\n\n// NewClient creates a new Bookstack API client with the provided configuration.\n// BaseURL, TokenID, and TokenSecret are required.\n//\n// Example:\n//\n// client := bookstack.NewClient(bookstack.Config{\n// BaseURL: \"https://docs.example.com\",\n// TokenID: \"token-id\",\n// TokenSecret: \"token-secret\",\n// })\nfunc NewClient(cfg Config) *Client\n```\n\n## Acceptance Criteria\n- [ ] Package documentation present\n- [ ] All exported types documented\n- [ ] All exported functions documented\n- [ ] Documentation renders correctly in pkg.go.dev style\n- [ ] No golint documentation warnings","status":"open","priority":1,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:40:24.752365504+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:40:24.752365504+01:00"}
|
||||||
{"id":"bookstack-api-cpg","title":"Implement PagesService (List, Get)","description":"Implement the PagesService with List and Get operations.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| List | GET /api/pages | All pages |\n| Get | GET /api/pages/{id} | Single page |\n\n## API Methods\n```go\ntype PagesService struct {\n client *Client\n}\n\nfunc (s *PagesService) List(ctx context.Context, opts *ListOptions) ([]*Page, error)\nfunc (s *PagesService) Get(ctx context.Context, id int) (*Page, error)\n```\n\n## Page Content\nThe Get response includes:\n- HTML content\n- RawHTML content \n- Markdown content (if available)\n\n## Acceptance Criteria\n- [ ] PagesService struct created\n- [ ] List() returns paginated pages\n- [ ] Get() returns single page by ID with content\n- [ ] Proper error handling (404 -\u003e ErrNotFound)\n- [ ] Unit tests with mock server","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:31.188568742+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:50:18.521622796+01:00","closed_at":"2026-01-30T09:50:18.521622796+01:00","close_reason":"Closed"}
|
{"id":"bookstack-api-cpg","title":"Implement PagesService (List, Get)","description":"Implement the PagesService with List and Get operations.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| List | GET /api/pages | All pages |\n| Get | GET /api/pages/{id} | Single page |\n\n## API Methods\n```go\ntype PagesService struct {\n client *Client\n}\n\nfunc (s *PagesService) List(ctx context.Context, opts *ListOptions) ([]*Page, error)\nfunc (s *PagesService) Get(ctx context.Context, id int) (*Page, error)\n```\n\n## Page Content\nThe Get response includes:\n- HTML content\n- RawHTML content \n- Markdown content (if available)\n\n## Acceptance Criteria\n- [ ] PagesService struct created\n- [ ] List() returns paginated pages\n- [ ] Get() returns single page by ID with content\n- [ ] Proper error handling (404 -\u003e ErrNotFound)\n- [ ] Unit tests with mock server","status":"closed","priority":0,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:31.188568742+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:50:18.521622796+01:00","closed_at":"2026-01-30T09:50:18.521622796+01:00","close_reason":"Closed"}
|
||||||
{"id":"bookstack-api-d2c","title":"Implement Pages Create and Update","description":"Implement write operations for Pages: Create and Update.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| Create | POST /api/pages | Create page |\n| Update | PUT /api/pages/{id} | Update page |\n\n## API Methods\n```go\ntype PageCreateRequest struct {\n BookID int `json:\"book_id\"`\n ChapterID int `json:\"chapter_id,omitempty\"`\n Name string `json:\"name\"`\n HTML string `json:\"html,omitempty\"`\n Markdown string `json:\"markdown,omitempty\"`\n}\n\ntype PageUpdateRequest struct {\n Name string `json:\"name,omitempty\"`\n HTML string `json:\"html,omitempty\"`\n Markdown string `json:\"markdown,omitempty\"`\n}\n\nfunc (s *PagesService) Create(ctx context.Context, req *PageCreateRequest) (*Page, error)\nfunc (s *PagesService) Update(ctx context.Context, id int, req *PageUpdateRequest) (*Page, error)\n```\n\n## Workflow (from PRD)\n1. Get page\n2. Edit content locally\n3. Update page\n\n## Acceptance Criteria\n- [ ] PageCreateRequest and PageUpdateRequest structs\n- [ ] Create() creates new page in book or chapter\n- [ ] Update() modifies existing page\n- [ ] Returns updated Page object\n- [ ] Proper error handling (400 -\u003e ErrBadRequest)\n- [ ] Unit tests with mock server","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:53.734987403+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:39:53.734987403+01:00"}
|
{"id":"bookstack-api-d2c","title":"Implement Pages Create and Update","description":"Implement write operations for Pages: Create and Update.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| Create | POST /api/pages | Create page |\n| Update | PUT /api/pages/{id} | Update page |\n\n## API Methods\n```go\ntype PageCreateRequest struct {\n BookID int `json:\"book_id\"`\n ChapterID int `json:\"chapter_id,omitempty\"`\n Name string `json:\"name\"`\n HTML string `json:\"html,omitempty\"`\n Markdown string `json:\"markdown,omitempty\"`\n}\n\ntype PageUpdateRequest struct {\n Name string `json:\"name,omitempty\"`\n HTML string `json:\"html,omitempty\"`\n Markdown string `json:\"markdown,omitempty\"`\n}\n\nfunc (s *PagesService) Create(ctx context.Context, req *PageCreateRequest) (*Page, error)\nfunc (s *PagesService) Update(ctx context.Context, id int, req *PageUpdateRequest) (*Page, error)\n```\n\n## Workflow (from PRD)\n1. Get page\n2. Edit content locally\n3. Update page\n\n## Acceptance Criteria\n- [ ] PageCreateRequest and PageUpdateRequest structs\n- [ ] Create() creates new page in book or chapter\n- [ ] Update() modifies existing page\n- [ ] Returns updated Page object\n- [ ] Proper error handling (400 -\u003e ErrBadRequest)\n- [ ] Unit tests with mock server","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:53.734987403+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:39:53.734987403+01:00"}
|
||||||
{"id":"bookstack-api-dd0","title":"Implement ChaptersService (List, Get)","description":"Implement the ChaptersService with List and Get operations.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| List | GET /api/chapters | All chapters |\n| Get | GET /api/chapters/{id} | Single chapter |\n\n## API Methods\n```go\ntype ChaptersService struct {\n client *Client\n}\n\nfunc (s *ChaptersService) List(ctx context.Context, opts *ListOptions) ([]*Chapter, error)\nfunc (s *ChaptersService) Get(ctx context.Context, id int) (*Chapter, error)\nfunc (s *ChaptersService) ListAll(ctx context.Context) iter.Seq2[*Chapter, error]\n```\n\n## Chapter Fields\n- ID, BookID, Name, Slug, Description, CreatedAt, UpdatedAt\n\n## Bookstack Hierarchy\nBook -\u003e Chapter -\u003e Page (Chapter is optional level)\n\n## Acceptance Criteria\n- [ ] ChaptersService struct created\n- [ ] List() returns paginated chapters\n- [ ] Get() returns single chapter by ID\n- [ ] ListAll() iterator implemented\n- [ ] Proper error handling\n- [ ] Unit tests with mock server","status":"open","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:53.235414205+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-28T09:39:53.235414205+01:00"}
|
{"id":"bookstack-api-dd0","title":"Implement ChaptersService (List, Get)","description":"Implement the ChaptersService with List and Get operations.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| List | GET /api/chapters | All chapters |\n| Get | GET /api/chapters/{id} | Single chapter |\n\n## API Methods\n```go\ntype ChaptersService struct {\n client *Client\n}\n\nfunc (s *ChaptersService) List(ctx context.Context, opts *ListOptions) ([]*Chapter, error)\nfunc (s *ChaptersService) Get(ctx context.Context, id int) (*Chapter, error)\nfunc (s *ChaptersService) ListAll(ctx context.Context) iter.Seq2[*Chapter, error]\n```\n\n## Chapter Fields\n- ID, BookID, Name, Slug, Description, CreatedAt, UpdatedAt\n\n## Bookstack Hierarchy\nBook -\u003e Chapter -\u003e Page (Chapter is optional level)\n\n## Acceptance Criteria\n- [ ] ChaptersService struct created\n- [ ] List() returns paginated chapters\n- [ ] Get() returns single chapter by ID\n- [ ] ListAll() iterator implemented\n- [ ] Proper error handling\n- [ ] Unit tests with mock server","status":"in_progress","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:53.235414205+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:52:15.978108435+01:00"}
|
||||||
{"id":"bookstack-api-jt9","title":"Implement Pages Export (Markdown, PDF)","description":"Implement page export functionality for Markdown and PDF formats.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| ExportMD | GET /api/pages/{id}/export/markdown | Markdown export |\n| ExportPDF | GET /api/pages/{id}/export/pdf | PDF export |\n\n## API Methods\n```go\nfunc (s *PagesService) ExportMarkdown(ctx context.Context, id int) ([]byte, error)\nfunc (s *PagesService) ExportPDF(ctx context.Context, id int) ([]byte, error)\n```\n\n## Technical Details\n- Markdown export returns plain text\n- PDF export returns binary data\n- Consider streaming for large PDFs (risk mitigation from PRD)\n- Return raw bytes, caller handles file writing\n\n## User Story\nUS4: Als Benutzer möchte ich eine Seite als Markdown oder PDF exportieren können.\n\n## Acceptance Criteria\n- [ ] ExportMarkdown() returns markdown content as bytes\n- [ ] ExportPDF() returns PDF content as bytes\n- [ ] Proper error handling (404 -\u003e ErrNotFound)\n- [ ] Large file handling considered\n- [ ] Unit tests with mock server","status":"in_progress","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:31.924404898+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:51:40.016505482+01:00"}
|
{"id":"bookstack-api-jt9","title":"Implement Pages Export (Markdown, PDF)","description":"Implement page export functionality for Markdown and PDF formats.\n\n## Requirements\nFrom PRD Section 5:\n\n| Method | Endpoint | Description |\n|--------|----------|-------------|\n| ExportMD | GET /api/pages/{id}/export/markdown | Markdown export |\n| ExportPDF | GET /api/pages/{id}/export/pdf | PDF export |\n\n## API Methods\n```go\nfunc (s *PagesService) ExportMarkdown(ctx context.Context, id int) ([]byte, error)\nfunc (s *PagesService) ExportPDF(ctx context.Context, id int) ([]byte, error)\n```\n\n## Technical Details\n- Markdown export returns plain text\n- PDF export returns binary data\n- Consider streaming for large PDFs (risk mitigation from PRD)\n- Return raw bytes, caller handles file writing\n\n## User Story\nUS4: Als Benutzer möchte ich eine Seite als Markdown oder PDF exportieren können.\n\n## Acceptance Criteria\n- [ ] ExportMarkdown() returns markdown content as bytes\n- [ ] ExportPDF() returns PDF content as bytes\n- [ ] Proper error handling (404 -\u003e ErrNotFound)\n- [ ] Large file handling considered\n- [ ] Unit tests with mock server","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:31.924404898+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:52:11.079682057+01:00","closed_at":"2026-01-30T09:52:11.079682057+01:00","close_reason":"Closed"}
|
||||||
{"id":"bookstack-api-m6n","title":"Implement pagination iterator (ListAll)","description":"Implement Go 1.23+ iterator pattern for memory-efficient pagination.\n\n## Requirements\nFrom PRD Section 5 (Pagination Iterator-Pattern):\n\n```go\n// ListAll returns an iterator over all entries\n// Uses Go 1.23+ iter.Seq or custom implementation\nfunc (s *BooksService) ListAll(ctx context.Context) iter.Seq2[*Book, error]\n\n// Usage:\nfor book, err := range client.Books.ListAll(ctx) {\n if err != nil {\n return err\n }\n fmt.Println(book.Name)\n}\n```\n\n## Technical Details\n- Go 1.23+ iter.Seq2 for yield-based iteration\n- Memory-efficient: only one page in memory at a time\n- Supports early break\n- Handle 10,000+ entries without memory issues\n\n## Pagination Parameters\n- count: 100 default, max 500\n- offset: auto-incremented per page\n- total: from API response to know when done\n\n## Services to Implement\n- BooksService.ListAll()\n- PagesService.ListAll()\n- ChaptersService.ListAll() (v0.2)\n- ShelvesService.ListAll() (v0.2)\n- SearchService.SearchAll() (v0.2)\n\n## Acceptance Criteria\n- [ ] iter.Seq2 based iterator implementation\n- [ ] Automatic pagination through all results\n- [ ] Early termination works correctly\n- [ ] Error propagation through iterator\n- [ ] Unit tests for pagination logic","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:31.682196545+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:51:35.690165915+01:00","closed_at":"2026-01-30T09:51:35.690165915+01:00","close_reason":"Closed"}
|
{"id":"bookstack-api-m6n","title":"Implement pagination iterator (ListAll)","description":"Implement Go 1.23+ iterator pattern for memory-efficient pagination.\n\n## Requirements\nFrom PRD Section 5 (Pagination Iterator-Pattern):\n\n```go\n// ListAll returns an iterator over all entries\n// Uses Go 1.23+ iter.Seq or custom implementation\nfunc (s *BooksService) ListAll(ctx context.Context) iter.Seq2[*Book, error]\n\n// Usage:\nfor book, err := range client.Books.ListAll(ctx) {\n if err != nil {\n return err\n }\n fmt.Println(book.Name)\n}\n```\n\n## Technical Details\n- Go 1.23+ iter.Seq2 for yield-based iteration\n- Memory-efficient: only one page in memory at a time\n- Supports early break\n- Handle 10,000+ entries without memory issues\n\n## Pagination Parameters\n- count: 100 default, max 500\n- offset: auto-incremented per page\n- total: from API response to know when done\n\n## Services to Implement\n- BooksService.ListAll()\n- PagesService.ListAll()\n- ChaptersService.ListAll() (v0.2)\n- ShelvesService.ListAll() (v0.2)\n- SearchService.SearchAll() (v0.2)\n\n## Acceptance Criteria\n- [ ] iter.Seq2 based iterator implementation\n- [ ] Automatic pagination through all results\n- [ ] Early termination works correctly\n- [ ] Error propagation through iterator\n- [ ] Unit tests for pagination logic","status":"closed","priority":1,"issue_type":"feature","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:31.682196545+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:51:35.690165915+01:00","closed_at":"2026-01-30T09:51:35.690165915+01:00","close_reason":"Closed"}
|
||||||
{"id":"bookstack-api-q8z","title":"Implement data types (Book, Page, Chapter, Shelf, SearchResult)","description":"Create all data structure types as defined in the PRD.\n\n## Requirements\nFrom PRD Section 4 (Datenstrukturen):\n\n```go\ntype Book struct {\n ID int `json:\"id\"`\n Name string `json:\"name\"`\n Slug string `json:\"slug\"`\n Description string `json:\"description\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n CreatedBy int `json:\"created_by\"`\n UpdatedBy int `json:\"updated_by\"`\n}\n\ntype Page struct {\n ID int `json:\"id\"`\n BookID int `json:\"book_id\"`\n ChapterID int `json:\"chapter_id\"`\n Name string `json:\"name\"`\n Slug string `json:\"slug\"`\n HTML string `json:\"html\"`\n RawHTML string `json:\"raw_html\"`\n Markdown string `json:\"markdown\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n}\n\ntype Chapter struct {\n ID int `json:\"id\"`\n BookID int `json:\"book_id\"`\n Name string `json:\"name\"`\n Slug string `json:\"slug\"`\n Description string `json:\"description\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n}\n\ntype Shelf struct {\n ID int `json:\"id\"`\n Name string `json:\"name\"`\n Slug string `json:\"slug\"`\n Description string `json:\"description\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n}\n\ntype SearchResult struct {\n ID int `json:\"id\"`\n Name string `json:\"name\"`\n Slug string `json:\"slug\"`\n Type string `json:\"type\"\\ // page, chapter, book, bookshelf\n URL string `json:\"url\"`\n Preview string `json:\"preview\"`\n}\n```\n\n## Acceptance Criteria\n- [ ] All types defined in types.go\n- [ ] JSON tags match Bookstack API format\n- [ ] time.Time fields parse correctly from API\n- [ ] GoDoc comments on all exported types","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:06.701609698+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:49:03.234639654+01:00","closed_at":"2026-01-30T09:49:03.234639654+01:00","close_reason":"Closed","comments":[{"id":3,"issue_id":"bookstack-api-q8z","author":"Oliver Jakoubek","text":"Plan: types.go already fully implemented with additional fields beyond PRD. Adding JSON round-trip test to verify time.Time parsing from API format.","created_at":"2026-01-30T08:48:46Z"}]}
|
{"id":"bookstack-api-q8z","title":"Implement data types (Book, Page, Chapter, Shelf, SearchResult)","description":"Create all data structure types as defined in the PRD.\n\n## Requirements\nFrom PRD Section 4 (Datenstrukturen):\n\n```go\ntype Book struct {\n ID int `json:\"id\"`\n Name string `json:\"name\"`\n Slug string `json:\"slug\"`\n Description string `json:\"description\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n CreatedBy int `json:\"created_by\"`\n UpdatedBy int `json:\"updated_by\"`\n}\n\ntype Page struct {\n ID int `json:\"id\"`\n BookID int `json:\"book_id\"`\n ChapterID int `json:\"chapter_id\"`\n Name string `json:\"name\"`\n Slug string `json:\"slug\"`\n HTML string `json:\"html\"`\n RawHTML string `json:\"raw_html\"`\n Markdown string `json:\"markdown\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n}\n\ntype Chapter struct {\n ID int `json:\"id\"`\n BookID int `json:\"book_id\"`\n Name string `json:\"name\"`\n Slug string `json:\"slug\"`\n Description string `json:\"description\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n}\n\ntype Shelf struct {\n ID int `json:\"id\"`\n Name string `json:\"name\"`\n Slug string `json:\"slug\"`\n Description string `json:\"description\"`\n CreatedAt time.Time `json:\"created_at\"`\n UpdatedAt time.Time `json:\"updated_at\"`\n}\n\ntype SearchResult struct {\n ID int `json:\"id\"`\n Name string `json:\"name\"`\n Slug string `json:\"slug\"`\n Type string `json:\"type\"\\ // page, chapter, book, bookshelf\n URL string `json:\"url\"`\n Preview string `json:\"preview\"`\n}\n```\n\n## Acceptance Criteria\n- [ ] All types defined in types.go\n- [ ] JSON tags match Bookstack API format\n- [ ] time.Time fields parse correctly from API\n- [ ] GoDoc comments on all exported types","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:06.701609698+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:49:03.234639654+01:00","closed_at":"2026-01-30T09:49:03.234639654+01:00","close_reason":"Closed","comments":[{"id":3,"issue_id":"bookstack-api-q8z","author":"Oliver Jakoubek","text":"Plan: types.go already fully implemented with additional fields beyond PRD. Adding JSON round-trip test to verify time.Time parsing from API format.","created_at":"2026-01-30T08:48:46Z"}]}
|
||||||
{"id":"bookstack-api-vl3","title":"Implement error types and APIError","description":"Create error handling infrastructure with sentinel errors and APIError type.\n\n## Requirements\nFrom PRD Section 9 (Error-Handling-Strategie):\n\n```go\nvar (\n ErrNotFound = errors.New(\"bookstack: resource not found\")\n ErrUnauthorized = errors.New(\"bookstack: unauthorized\")\n ErrForbidden = errors.New(\"bookstack: forbidden\")\n ErrRateLimited = errors.New(\"bookstack: rate limited\")\n ErrBadRequest = errors.New(\"bookstack: bad request\")\n)\n\ntype APIError struct {\n StatusCode int\n Code int `json:\"code\"`\n Message string `json:\"message\"`\n Body []byte\n}\n\nfunc (e *APIError) Error() string\nfunc (e *APIError) Is(target error) bool // Maps status codes to sentinel errors\n```\n\n## Status Code Mapping\n- 400 -\u003e ErrBadRequest\n- 401 -\u003e ErrUnauthorized\n- 403 -\u003e ErrForbidden\n- 404 -\u003e ErrNotFound\n- 429 -\u003e ErrRateLimited\n\n## Acceptance Criteria\n- [ ] All sentinel errors defined\n- [ ] APIError struct with Error() method\n- [ ] APIError.Is() correctly maps status codes\n- [ ] errors.Is() works with wrapped APIError\n- [ ] Unit tests for error matching","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:06.467984697+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:48:32.685138971+01:00","closed_at":"2026-01-30T09:48:32.685138971+01:00","close_reason":"Closed","comments":[{"id":2,"issue_id":"bookstack-api-vl3","author":"Oliver Jakoubek","text":"Plan: errors.go already implemented. Need to add unit tests for APIError.Error(), APIError.Is(), and errors.Is() integration. Sentinel error messages use short form without prefix which is fine.","created_at":"2026-01-30T08:48:14Z"}]}
|
{"id":"bookstack-api-vl3","title":"Implement error types and APIError","description":"Create error handling infrastructure with sentinel errors and APIError type.\n\n## Requirements\nFrom PRD Section 9 (Error-Handling-Strategie):\n\n```go\nvar (\n ErrNotFound = errors.New(\"bookstack: resource not found\")\n ErrUnauthorized = errors.New(\"bookstack: unauthorized\")\n ErrForbidden = errors.New(\"bookstack: forbidden\")\n ErrRateLimited = errors.New(\"bookstack: rate limited\")\n ErrBadRequest = errors.New(\"bookstack: bad request\")\n)\n\ntype APIError struct {\n StatusCode int\n Code int `json:\"code\"`\n Message string `json:\"message\"`\n Body []byte\n}\n\nfunc (e *APIError) Error() string\nfunc (e *APIError) Is(target error) bool // Maps status codes to sentinel errors\n```\n\n## Status Code Mapping\n- 400 -\u003e ErrBadRequest\n- 401 -\u003e ErrUnauthorized\n- 403 -\u003e ErrForbidden\n- 404 -\u003e ErrNotFound\n- 429 -\u003e ErrRateLimited\n\n## Acceptance Criteria\n- [ ] All sentinel errors defined\n- [ ] APIError struct with Error() method\n- [ ] APIError.Is() correctly maps status codes\n- [ ] errors.Is() works with wrapped APIError\n- [ ] Unit tests for error matching","status":"closed","priority":0,"issue_type":"task","owner":"mail@oliverjakoubek.de","created_at":"2026-01-28T09:39:06.467984697+01:00","created_by":"Oliver Jakoubek","updated_at":"2026-01-30T09:48:32.685138971+01:00","closed_at":"2026-01-30T09:48:32.685138971+01:00","close_reason":"Closed","comments":[{"id":2,"issue_id":"bookstack-api-vl3","author":"Oliver Jakoubek","text":"Plan: errors.go already implemented. Need to add unit tests for APIError.Error(), APIError.Is(), and errors.Is() integration. Sentinel error messages use short form without prefix which is fine.","created_at":"2026-01-30T08:48:14Z"}]}
|
||||||
|
|
|
||||||
50
chapters.go
50
chapters.go
|
|
@ -1,6 +1,10 @@
|
||||||
package bookstack
|
package bookstack
|
||||||
|
|
||||||
import "context"
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
)
|
||||||
|
|
||||||
// ChaptersService handles operations on chapters.
|
// ChaptersService handles operations on chapters.
|
||||||
type ChaptersService struct {
|
type ChaptersService struct {
|
||||||
|
|
@ -8,36 +12,26 @@ type ChaptersService struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns a list of chapters with optional filtering.
|
// List returns a list of chapters with optional filtering.
|
||||||
// TODO: Implement API call to GET /api/chapters
|
|
||||||
func (s *ChaptersService) List(ctx context.Context, opts *ListOptions) ([]Chapter, error) {
|
func (s *ChaptersService) List(ctx context.Context, opts *ListOptions) ([]Chapter, error) {
|
||||||
// Placeholder for future implementation
|
var resp listResponse[Chapter]
|
||||||
return nil, nil
|
err := s.client.do(ctx, "GET", "/api/chapters"+opts.queryString(), nil, &resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.Data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAll returns an iterator over all chapters, handling pagination automatically.
|
||||||
|
func (s *ChaptersService) ListAll(ctx context.Context) iter.Seq2[Chapter, error] {
|
||||||
|
return listAll[Chapter](ctx, s.client, "/api/chapters")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get retrieves a single chapter by ID.
|
// Get retrieves a single chapter by ID.
|
||||||
// TODO: Implement API call to GET /api/chapters/{id}
|
|
||||||
func (s *ChaptersService) Get(ctx context.Context, id int) (*Chapter, error) {
|
func (s *ChaptersService) Get(ctx context.Context, id int) (*Chapter, error) {
|
||||||
// Placeholder for future implementation
|
var chapter Chapter
|
||||||
return nil, nil
|
err := s.client.do(ctx, "GET", fmt.Sprintf("/api/chapters/%d", id), nil, &chapter)
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
// Create creates a new chapter.
|
}
|
||||||
// TODO: Implement API call to POST /api/chapters
|
return &chapter, nil
|
||||||
func (s *ChaptersService) Create(ctx context.Context, chapter *Chapter) (*Chapter, error) {
|
|
||||||
// Placeholder for future implementation
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update updates an existing chapter.
|
|
||||||
// TODO: Implement API call to PUT /api/chapters/{id}
|
|
||||||
func (s *ChaptersService) Update(ctx context.Context, id int, chapter *Chapter) (*Chapter, error) {
|
|
||||||
// Placeholder for future implementation
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete deletes a chapter by ID.
|
|
||||||
// TODO: Implement API call to DELETE /api/chapters/{id}
|
|
||||||
func (s *ChaptersService) Delete(ctx context.Context, id int) error {
|
|
||||||
// Placeholder for future implementation
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
62
chapters_test.go
Normal file
62
chapters_test.go
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
package bookstack
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestChaptersService_List(t *testing.T) {
|
||||||
|
c := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/api/chapters" {
|
||||||
|
t.Errorf("path = %s, want /api/chapters", r.URL.Path)
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"data": []map[string]any{{"id": 1, "name": "Ch 1", "book_id": 1}},
|
||||||
|
"total": 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
chapters, err := c.Chapters.List(context.Background(), nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if len(chapters) != 1 {
|
||||||
|
t.Fatalf("got %d chapters, want 1", len(chapters))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChaptersService_Get(t *testing.T) {
|
||||||
|
c := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/api/chapters/3" {
|
||||||
|
t.Errorf("path = %s, want /api/chapters/3", r.URL.Path)
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"id": 3, "name": "Chapter Three", "book_id": 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
ch, err := c.Chapters.Get(context.Background(), 3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if ch.Name != "Chapter Three" {
|
||||||
|
t.Errorf("Name = %q, want %q", ch.Name, "Chapter Three")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChaptersService_Get_NotFound(t *testing.T) {
|
||||||
|
c := testClient(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"error": map[string]string{"message": "Chapter not found"},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
_, err := c.Chapters.Get(context.Background(), 999)
|
||||||
|
if !errors.Is(err, ErrNotFound) {
|
||||||
|
t.Errorf("expected ErrNotFound, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue