Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

| Component | Status | Details |
|-----------|--------|---------|
| **Smithy Spec** | 175 operations | Single source of truth for all APIs |
| **Smithy Spec** | 204 operations | Single source of truth for all APIs |
| **Go SDK** | Production-ready | Full generated client + service wrappers |
| **TypeScript SDK** | Production-ready | 37 generated services, openapi-fetch based |
| **Ruby SDK** | Production-ready | 37 generated services |
Expand All @@ -31,7 +31,7 @@ Smithy Spec → OpenAPI → Generated Client → Service Layer → User
| **Kotlin** | Ktor via `BaseService` | `sdk/src/commonMain/kotlin/.../generated/services/*.kt` |
| **Python** | httpx via `HttpClient` | `src/basecamp/generated/services/*.py` |

All 175 operations across 38+ services are generated. Hand-written code is limited to infrastructure:
All 204 operations across 38+ services are generated. Hand-written code is limited to infrastructure:

| Purpose | TypeScript | Ruby | Swift | Kotlin | Python |
|---------|-----------|------|-------|--------|--------|
Expand Down
8 changes: 4 additions & 4 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ END

### behavior-model.json Retry Patterns

All 181 operations in `behavior-model.json` use `retry_on: [429, 503]`. Three `(max, base_delay_ms)` patterns exist:
All 204 operations in `behavior-model.json` use `retry_on: [429, 503]`. Three `(max, base_delay_ms)` patterns exist:
- `(2, 1000)` — most create operations
- `(3, 1000)` — most read/update/delete operations
- `(3, 2000)` — `CreateAttachment`, `CreateCampfireUpload` (file uploads)
Expand Down Expand Up @@ -1504,9 +1504,9 @@ Every operation has a `retry` block, including non-idempotent POSTs. For non-ide

### Operation Counts

- Total operations: 181
- Idempotent: 55 (flagged with `idempotent: true`)
- Non-idempotent: 126 (no `idempotent` field, or not present)
- Total operations: 204
- Idempotent: 66 (flagged with `idempotent: true`)
- Non-idempotent: 138 (no `idempotent` field, or not present)
- All operations use `retry_on: [429, 503]`

---
Expand Down
12 changes: 12 additions & 0 deletions behavior-model.json
Original file line number Diff line number Diff line change
Expand Up @@ -2257,6 +2257,18 @@
]
}
},
"UpdateCampfireLine": {
"idempotent": true,
"retry": {
"max": 3,
"base_delay_ms": 1000,
"backoff": "exponential",
"retry_on": [
429,
503
]
}
},
"UpdateCard": {
"idempotent": true,
"retry": {
Expand Down
2 changes: 1 addition & 1 deletion go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ cfg, err := basecamp.LoadConfig("/path/to/config.json")
| `MessageBoards()` | Get |
| `MessageTypes()` | List, Get, Create, Update, Destroy |
| `Comments()` | List, Get, Create, Update, Trash |
| `Campfires()` | List, Get, ListLines, GetLine, CreateLine, DeleteLine, Chatbot CRUD |
| `Campfires()` | List, Get, ListLines, GetLine, CreateLine, UpdateLine, DeleteLine, Chatbot CRUD |
| `Forwards()` | List, Get |

### Scheduling
Expand Down
38 changes: 38 additions & 0 deletions go/pkg/basecamp/campfires.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,44 @@ func (s *CampfiresService) CreateLine(ctx context.Context, campfireID int64, con
return &line, nil
}

// UpdateLine updates the content of an existing line (message) in a campfire.
// The content is always treated as rich text (HTML) — the server coerces every
// edited line to rich text — and only the line's creator may edit it.
// The API returns 204 No Content on success; if the caller needs the updated
// representation, follow up with GetLine. Mirrors DeleteLine in returning only
// an error so a transient post-mutation read can't make a successful update
// appear to fail.
func (s *CampfiresService) UpdateLine(ctx context.Context, campfireID, lineID int64, content string) (err error) {
op := OperationInfo{
Service: "Campfires", Operation: "UpdateLine",
ResourceType: "campfire_line", IsMutation: true,
ResourceID: lineID,
}
if gater, ok := s.client.parent.hooks.(GatingHooks); ok {
if ctx, err = gater.OnOperationGate(ctx, op); err != nil {
return
}
}
start := time.Now()
ctx = s.client.parent.hooks.OnOperationStart(ctx, op)
defer func() { s.client.parent.hooks.OnOperationEnd(ctx, op, err, time.Since(start)) }()

if content == "" {
err = ErrUsage("campfire line content is required")
return err
}

body := generated.UpdateCampfireLineJSONRequestBody{
Content: content,
}

resp, err := s.client.parent.gen.UpdateCampfireLineWithResponse(ctx, s.client.accountID, campfireID, lineID, body)
if err != nil {
return err
}
return checkResponse(resp.HTTPResponse, resp.Body)
}

// DeleteLine deletes a line (message) from a campfire.
func (s *CampfiresService) DeleteLine(ctx context.Context, campfireID, lineID int64) (err error) {
op := OperationInfo{
Expand Down
48 changes: 48 additions & 0 deletions go/pkg/basecamp/campfires_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,54 @@ func TestCreateLine_PlainOption_Service(t *testing.T) {
}
}

func TestUpdateLine_EmptyContent(t *testing.T) {
svc := testCampfiresServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Errorf("server should not be called when content is empty")
})
if err := svc.UpdateLine(context.Background(), 200, 1069479350, ""); err == nil {
t.Fatalf("expected error for empty content")
}
}

func TestUpdateLine_Service(t *testing.T) {
var receivedBody map[string]any
var receivedMethod, receivedPath string
svc := testCampfiresServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "PUT" {
t.Errorf("expected PUT only, got %s", r.Method)
return
}
receivedMethod = r.Method
receivedPath = r.URL.Path
body, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("failed to read request body: %v", err)
return
}
if err := json.Unmarshal(body, &receivedBody); err != nil {
t.Errorf("failed to unmarshal request body: %v", err)
return
}
w.WriteHeader(204)
})

if err := svc.UpdateLine(context.Background(), 200, 1069479350, "Edited!"); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if receivedMethod != "PUT" {
t.Errorf("expected PUT, got %s", receivedMethod)
}
if receivedPath != "/99999/chats/200/lines/1069479350" {
t.Errorf("unexpected path: %s", receivedPath)
}
if receivedBody["content"] != "Edited!" {
t.Errorf("expected content 'Edited!', got %v", receivedBody["content"])
}
if len(receivedBody) != 1 {
t.Errorf("expected body to contain only content, got %v", receivedBody)
}
}

func TestChatbot_UnmarshalList(t *testing.T) {
data := loadCampfiresFixture(t, "chatbots_list.json")

Expand Down
3 changes: 2 additions & 1 deletion go/pkg/basecamp/url-routes.json
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,8 @@
"resource": "Campfire",
"operations": {
"DELETE": "DeleteCampfireLine",
"GET": "GetCampfireLine"
"GET": "GetCampfireLine",
"PUT": "UpdateCampfireLine"
},
"params": {
"accountId": {
Expand Down
Loading
Loading