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
95 changes: 95 additions & 0 deletions .agents/skills/go-types-conversion/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
name: go-types-conversion
description: Naming convention for type-translation files and functions. Use when creating or editing files that convert between domain, API, and DB types.
user-invocable: true
allowed-tools: Read, Edit, Write, Grep, Glob
---

# Type Translation Naming

Apply to new and touched code. Do not rename legacy symbols unsolicited.

## File naming

| Path contains | File name | Purpose |
| ------------------------------------------------- | ------------ | ------------ |
| `httpdriver/`, `httphandler/`, `api/v3/handlers/` | `convert.go` | API ↔ domain |
| `adapter/`, `repo/` | `mapping.go` | DB ↔ domain |

Split large files by entity: `convert_plan.go`, `mapping_subscription.go`.

`mapper.go` is forbidden. Rename it to `convert.go` or `mapping.go` (based on layer) when the file is touched.

## Function naming

### Shape: `From<Qualifier><Thing>` / `To<Qualifier><Thing>`

The qualifier is `API` or `DB` — no other qualifiers (`Domain`, `Model`, package-name infixes).

The suffix `<Thing>` is the **non-domain type's unqualified name** — the API type or DB type, not the domain type. This keeps it stable: a matched pair (`FromAPI<Thing>` / `ToAPI<Thing>`) always refers to the same non-domain type, regardless of direction.

- `FromAPI<Thing>` — takes the API type `<Thing>` as input, returns the domain representation.
- `ToAPI<Thing>` — takes the domain type as input, returns the API type `<Thing>`.
- Same for `FromDB<Thing>` / `ToDB<Thing>`.

### Examples

```go
// API ↔ domain
FromAPIPlan(a api.Plan) (plan.Plan, error)
ToAPIPlan(p plan.Plan) api.Plan

// Suffix is the API type name, even when domain type differs
FromAPIPlanCreate(a api.PlanCreate) (plan.CreateInput, error)
ToAPIPlanCreate(p plan.CreateInput) api.PlanCreate

FromAPIProRatingConfig(a api.ProRatingConfig) (productcatalog.ProRatingConfig, error)
ToAPIProRatingConfig(p productcatalog.ProRatingConfig) *api.ProRatingConfig

// DB ↔ domain — suffix is the DB type name
FromDBSubscription(row *db.Subscription) (subscription.Subscription, error)
ToDBSubscription(s subscription.Subscription) *db.Subscription

FromDBChargeFlatFee(row *entdb.ChargeFlatFee) (flatfee.Charge, error)
ToDBChargeFlatFee(c flatfee.Charge) *entdb.ChargeFlatFee
```

### Additional rules

- **Exported** functions always include the type suffix (`FromAPIPlanCreate`, not bare `FromAPI`).
- **Unexported** helpers in a single-type file may drop the suffix (`fromDB`, `toAPI`).
- **Fallible** (parse/validate) → `(T, error)`. **Infallible** (projection) → `T`. Typically `FromAPI…` / `FromDB…` is fallible; the reverse is not.
- **Batch helpers** use the plural: `FromAPIPlans`, `ToDBSubscriptions`. Same suffix rule — the plural of the non-domain type name.

### Forbidden patterns

- `Map…`, `Convert…To…`, primary `As…`
- `<Source>To<Target>` shape (e.g. `APIToPlan`)
- Bare `FromAPI` / `ToDB` without a type suffix
- goverter or other codegen type mappers

## Decision tree

### Naming a function

1. **Pick the qualifier:** API/HTTP/wire on one side → `API`. DB/persistence on one side → `DB`.
2. **Pick the suffix:** the non-domain type's unqualified name (`Plan`, `PlanCreate`, `ChargeFlatFee`).
3. **Pick the return style:** fallible → `(T, error)`, infallible → `T`.
4. **Exported?** Must include the type suffix. Unexported single-type helper may drop it.

### Interacting with the user

- **File is `mapper.go`?** Flag it — should be `convert.go` or `mapping.go` based on layer. Offer to rename as part of the edit. Don't rename silently.
- **Adding new functions to a legacy file?** Use the new convention for new functions. Don't rename old ones unless asked.
- **Task is "clean up this file"?** Rename, update call sites, `Grep` for the old name to catch misses, keep the rename in its own commit.
- **`// Code generated` header?** Off-limits regardless.

## Suggestion phrasing

Lead with the specific rename and the reason. Keep it short.

> `MapChargeFlatFeeFromDB` — use `FromDBChargeFlatFee`. Want me to rename and update callers?

> Direction looks inverted — `FromAPI…` returns a domain type, so this should be `ToAPIPlan`. Drop the error return if it can't actually fail.

Comment thread
tothandras marked this conversation as resolved.
> This file is `mapper.go` — should be `convert.go` since it lives in `httphandler/`. Want me to rename it?
40 changes: 20 additions & 20 deletions api/v3/handlers/apps/convert.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 16 additions & 16 deletions api/v3/handlers/apps/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,27 @@ import (
// goverter:useUnderlyingTypeMethods
// goverter:matchIgnoreCase
// goverter:extend IntToFloat32
// goverter:extend MapAppToAPI
// goverter:extend ToAPIBillingApp
// goverter:enum:unknown @error
var (
ConvertToListAppResponse func(source response.PagePaginationResponse[api.BillingApp]) api.AppPagePaginatedResponse
ToAPIAppPagePaginatedResponse func(source response.PagePaginationResponse[api.BillingApp]) api.AppPagePaginatedResponse

ConvertMarketplaceListingToV3Api func(source app.MarketplaceListing) (api.BillingAppCatalogItem, error)
ToAPIBillingAppCatalogItem func(source app.MarketplaceListing) (api.BillingAppCatalogItem, error)

// goverter:enum:map AppTypeStripe BillingAppTypeStripe
// goverter:enum:map AppTypeSandbox BillingAppTypeSandbox
// goverter:enum:map AppTypeCustomInvoicing BillingAppTypeExternalInvoicing
ConvertAppTypeToV3Api func(source app.AppType) (api.BillingAppType, error)
ToAPIBillingAppTypeFromDomain func(source app.AppType) (api.BillingAppType, error)

ConvertAppsToBillingApps func(source []app.App) ([]api.BillingApp, error)
ToAPIBillingApps func(source []app.App) ([]api.BillingApp, error)
)

func IntToFloat32(i int) float32 {
return float32(i)
}

// MapAppToAPI maps an app to an v3 API app
func MapAppToAPI(item app.App) (api.BillingApp, error) {
// ToAPIBillingApp maps an app to a v3 API app
func ToAPIBillingApp(item app.App) (api.BillingApp, error) {
if item == nil {
return api.BillingApp{}, errors.New("invalid app: nil")
}
Expand All @@ -55,7 +55,7 @@ func MapAppToAPI(item app.App) (api.BillingApp, error) {
return api.BillingApp{}, fmt.Errorf("expected stripe app, got %T", item)
}

billingAppStripe, err := mapStripeAppToAPI(stripeApp.Meta)
billingAppStripe, err := toAPIBillingAppStripe(stripeApp.Meta)
if err != nil {
return api.BillingApp{}, fmt.Errorf("failed to map stripe app to API: %w", err)
}
Expand All @@ -72,7 +72,7 @@ func MapAppToAPI(item app.App) (api.BillingApp, error) {
return api.BillingApp{}, fmt.Errorf("expected sandbox app, got %T", item)
}

billingAppSandbox, err := mapSandboxAppToAPI(sandboxApp.Meta)
billingAppSandbox, err := toAPIBillingAppSandbox(sandboxApp.Meta)
if err != nil {
return api.BillingApp{}, fmt.Errorf("failed to map sandbox app to API: %w", err)
}
Expand All @@ -89,7 +89,7 @@ func MapAppToAPI(item app.App) (api.BillingApp, error) {
return api.BillingApp{}, fmt.Errorf("expected custom invoicing app, got %T", item)
}

billingAppExternalInvoicing, err := mapCustomInvoicingAppToAPI(customInvoicingApp.Meta)
billingAppExternalInvoicing, err := toAPIBillingAppExternalInvoicing(customInvoicingApp.Meta)
if err != nil {
return api.BillingApp{}, fmt.Errorf("failed to map custom invoicing app to API: %w", err)
}
Expand All @@ -105,8 +105,8 @@ func MapAppToAPI(item app.App) (api.BillingApp, error) {
}
}

func mapSandboxAppToAPI(sandboxApp appsandbox.Meta) (api.BillingAppSandbox, error) {
definition, err := ConvertMarketplaceListingToV3Api(sandboxApp.GetListing())
func toAPIBillingAppSandbox(sandboxApp appsandbox.Meta) (api.BillingAppSandbox, error) {
definition, err := ToAPIBillingAppCatalogItem(sandboxApp.GetListing())
if err != nil {
return api.BillingAppSandbox{}, err
}
Expand All @@ -125,10 +125,10 @@ func mapSandboxAppToAPI(sandboxApp appsandbox.Meta) (api.BillingAppSandbox, erro
}, nil
}

func mapStripeAppToAPI(
func toAPIBillingAppStripe(
stripeApp appstripeentityapp.Meta,
) (api.BillingAppStripe, error) {
definition, err := ConvertMarketplaceListingToV3Api(stripeApp.GetListing())
definition, err := ToAPIBillingAppCatalogItem(stripeApp.GetListing())
if err != nil {
return api.BillingAppStripe{}, err
}
Expand All @@ -153,8 +153,8 @@ func mapStripeAppToAPI(
return apiStripeApp, nil
}

func mapCustomInvoicingAppToAPI(customInvoicingApp appcustominvoicing.Meta) (api.BillingAppExternalInvoicing, error) {
definition, err := ConvertMarketplaceListingToV3Api(customInvoicingApp.GetListing())
func toAPIBillingAppExternalInvoicing(customInvoicingApp appcustominvoicing.Meta) (api.BillingAppExternalInvoicing, error) {
definition, err := ToAPIBillingAppCatalogItem(customInvoicingApp.GetListing())
if err != nil {
return api.BillingAppExternalInvoicing{}, err
}
Expand Down
2 changes: 1 addition & 1 deletion api/v3/handlers/apps/get_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (h *handler) GetApp() GetAppHandler {
return GetAppResponse{}, fmt.Errorf("failed to get app: %w", err)
}

return MapAppToAPI(app)
return ToAPIBillingApp(app)
},
commonhttp.JSONResponseEncoder[GetAppResponse],
httptransport.AppendOptions(
Expand Down
4 changes: 2 additions & 2 deletions api/v3/handlers/apps/list_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func (h *handler) ListApps() ListAppsHandler {
return ListAppsResponse{}, fmt.Errorf("failed to list apps: %w", err)
}

items, err := ConvertAppsToBillingApps(result.Items)
items, err := ToAPIBillingApps(result.Items)
if err != nil {
return ListAppsResponse{}, fmt.Errorf("failed to convert Apps to BillingApps: %w", err)
}
Expand All @@ -74,7 +74,7 @@ func (h *handler) ListApps() ListAppsHandler {
Total: lo.ToPtr(result.TotalCount),
})

response := ConvertToListAppResponse(r)
response := ToAPIAppPagePaginatedResponse(r)

return response, nil
},
Expand Down
Loading
Loading