Skip to content
Open
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
12 changes: 12 additions & 0 deletions behavior-model.json
Original file line number Diff line number Diff line change
Expand Up @@ -2241,6 +2241,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 @@ -174,7 +174,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
57 changes: 57 additions & 0 deletions go/pkg/basecamp/campfires.go
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,63 @@ func (s *CampfiresService) CreateLine(ctx context.Context, campfireID int64, con
return &line, nil
}

// UpdateLineOptions specifies optional parameters for updating a campfire line.
type UpdateLineOptions struct {
// ContentType is "text/plain" or "text/html". If empty, the API defaults to plain text.
ContentType string
}

// UpdateLine updates the content of an existing line (message) in a campfire.
// opts is optional; pass an UpdateLineOptions to set content_type (text/html or text/plain).
// 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, opts ...*UpdateLineOptions) (err error) {
Comment thread
nnemirovsky marked this conversation as resolved.
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 len(opts) > 1 {
err = ErrUsage("UpdateLine accepts at most one UpdateLineOptions argument")
return err
}

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

body := generated.UpdateCampfireLineJSONRequestBody{
Content: content,
}
if len(opts) > 0 && opts[0] != nil && opts[0].ContentType != "" {
switch opts[0].ContentType {
case LineContentTypePlain, LineContentTypeHTML:
body.ContentType = opts[0].ContentType
default:
err = ErrUsage("content_type must be \"text/plain\" or \"text/html\"")
return err
}
}

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
86 changes: 86 additions & 0 deletions go/pkg/basecamp/campfires_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -663,6 +663,92 @@ 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_MultipleOptions(t *testing.T) {
svc := testCampfiresServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Errorf("server should not be called when multiple options are provided")
})
err := svc.UpdateLine(context.Background(), 200, 1069479350, "x",
&UpdateLineOptions{ContentType: LineContentTypeHTML},
&UpdateLineOptions{ContentType: LineContentTypePlain})
if err == nil {
t.Fatalf("expected error for multiple options")
}
}

func TestUpdateLine_InvalidContentType(t *testing.T) {
svc := testCampfiresServer(t, func(w http.ResponseWriter, r *http.Request) {
t.Errorf("server should not be called when content_type is invalid")
})
err := svc.UpdateLine(context.Background(), 200, 1069479350, "x",
&UpdateLineOptions{ContentType: "application/xml"})
if err == nil {
t.Fatalf("expected error for invalid content_type")
}
}

func TestUpdateLine_NoOptions_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, _ := io.ReadAll(r.Body)
json.Unmarshal(body, &receivedBody)
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 _, exists := receivedBody["content_type"]; exists {
t.Errorf("content_type should be absent with no options, got %v", receivedBody["content_type"])
}
}

func TestUpdateLine_HTMLOption_Service(t *testing.T) {
var receivedBody map[string]any
svc := testCampfiresServer(t, func(w http.ResponseWriter, r *http.Request) {
if r.Method != "PUT" {
t.Errorf("expected PUT only, got %s", r.Method)
return
}
body, _ := io.ReadAll(r.Body)
json.Unmarshal(body, &receivedBody)
w.WriteHeader(204)
})

err := svc.UpdateLine(context.Background(), 200, 1069479350, "<b>Hi</b>",
&UpdateLineOptions{ContentType: LineContentTypeHTML})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if receivedBody["content_type"] != "text/html" {
t.Errorf("expected content_type 'text/html', got %v", receivedBody["content_type"])
}
}

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

Expand Down
Loading
Loading