Documentation
¶
Overview ¶
Package serviceutil provides shared, generic utilities used across all service handlers. The goal is to ensure every service follows the same patterns for request parsing, response writing, error handling, and logging — so that reading any service handler feels immediately familiar.
TypeScript analogy: this is the equivalent of a shared utils/ or lib/ directory that every route handler imports from.
Usage pattern in a handler:
func (h *Handler) CreateQueue(w http.ResponseWriter, r *http.Request) {
var req createQueueRequest
if !serviceutil.DecodeJSON(w, r, &req) {
return // error already written
}
if !serviceutil.RequireFields(w, r, req.QueueName, "QueueName") {
return
}
// ... implementation
}
Index ¶
- Variables
- func ARNRegion(arn string) string
- func AlphaNumericHyphenUnderscorePeriod(c rune) bool
- func AppSyncDataSourceName(name string) *protocol.AWSError
- func AppSyncFieldName(name string) *protocol.AWSError
- func AppSyncFunctionName(name string) *protocol.AWSError
- func AppSyncGraphQLAPIName(name string) *protocol.AWSError
- func AppSyncIdentifierName(name, field string) *protocol.AWSError
- func AppSyncTypeName(name string) *protocol.AWSError
- func BucketName(name string) *protocol.AWSError
- func ClampInt(v, min, max int) int
- func DecodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool
- func DefaultInt(v, defaultVal int) int
- func HasQueryParam(r *http.Request, param string) bool
- func HeaderPrefix(r *http.Request, prefix string) map[string]string
- func ParseIntDefault(s string, defaultVal int) int
- func QueryInt(r *http.Request, param string, defaultVal int) int
- func QueryString(r *http.Request, param, defaultVal string) string
- func QueueName(name string) *protocol.AWSError
- func RegionKey(region, key string) string
- func RequireString(w http.ResponseWriter, r *http.Request, value, paramName string) bool
- func ResourceName(name string, rule NameRule) *protocol.AWSError
- func SplitRegionKey(key string) (region, rest string)
- func TableName(name string) *protocol.AWSError
- func ValidateAndRespond(w http.ResponseWriter, r *http.Request, aerr *protocol.AWSError) bool
- type LazyInit
- type NameRule
- type Page
- type ServiceLogger
- func (l *ServiceLogger) Debug(msg string, fields ...zap.Field)
- func (l *ServiceLogger) Error(msg string, fields ...zap.Field)
- func (l *ServiceLogger) Info(msg string, fields ...zap.Field)
- func (l *ServiceLogger) LogStateError(r *http.Request, op string, aerr *protocol.AWSError, fields ...zap.Field)
- func (l *ServiceLogger) Logger() *zap.Logger
- func (l *ServiceLogger) Warn(msg string, fields ...zap.Field)
- func (l *ServiceLogger) With(fields ...zap.Field) *ServiceLogger
- func (l *ServiceLogger) WithOperation(op string) *ServiceLogger
- func (l *ServiceLogger) ZapLogger() *zap.Logger
Constants ¶
This section is empty.
Variables ¶
var ( // GraphQLIdentifierPattern matches AppSync GraphQL identifiers documented for // data source, function, type, and field names. // https://docs.aws.amazon.com/appsync/latest/APIReference/API_CreateDataSource.html GraphQLIdentifierPattern = regexp.MustCompile(`^[_A-Za-z][_0-9A-Za-z]*$`) )
Functions ¶
func ARNRegion ¶
ARNRegion extracts the region component from an AWS ARN (the 4th colon-separated field, e.g. "us-east-1"). Returns an empty string for non-ARN inputs or ARNs without a region field.
func AlphaNumericHyphenUnderscorePeriod ¶
AlphaNumericHyphenUnderscorePeriod matches the common AWS identifier alphabet used by SQS queue names and DynamoDB table names.
func AppSyncDataSourceName ¶
func AppSyncFieldName ¶
func AppSyncFunctionName ¶
func AppSyncGraphQLAPIName ¶
AppSyncGraphQLAPIName validates the required GraphQL API name. AWS documents this field as required without publishing a stricter length or pattern.
func AppSyncIdentifierName ¶
AppSyncIdentifierName validates AppSync names documented with the [_A-Za-z][_0-9A-Za-z]* pattern and 1..65536 length constraint.
func AppSyncTypeName ¶
func BucketName ¶
BucketName validates an S3 bucket name against AWS naming rules. Returns nil if valid, or a *protocol.AWSError with code "InvalidBucketName".
func ClampInt ¶
ClampInt returns v clamped to [min, max].
maxMessages := serviceutil.ClampInt(req.MaxNumberOfMessages, 1, 10)
func DecodeJSON ¶
DecodeJSON decodes the request body as JSON into dst. On failure, writes a well-formed AWS JSON error response and returns false. The caller should return immediately when false is returned — the response has already been written.
This replaces the repetitive pattern:
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
protocol.WriteJSONError(w, r, protocol.ErrInvalidArgument("invalid request body"))
return
}
func DefaultInt ¶
DefaultInt returns v if v > 0, otherwise defaultVal. Useful for optional integer request fields that default to a non-zero value.
timeout := serviceutil.DefaultInt(req.VisibilityTimeout, 30)
func HasQueryParam ¶
HasQueryParam returns true if the named query parameter is present in the URL, regardless of its value. Useful for S3-style action parameters like ?location.
if serviceutil.HasQueryParam(r, "location") { ... }
func HeaderPrefix ¶
HeaderPrefix extracts all headers whose names start with prefix and returns them as a map with the prefix stripped from each key name. Keys are lowercased for consistency.
Used by S3 to extract x-amz-meta-* user metadata headers:
meta := serviceutil.HeaderPrefix(r, "X-Amz-Meta-")
// {"author": "alice", "version": "1.0"}
func ParseIntDefault ¶
ParseIntDefault parses a string as an integer. Returns defaultVal if the string is empty or unparseable.
timeout := serviceutil.ParseIntDefault(q.Attributes["VisibilityTimeout"], 30)
func QueryInt ¶
QueryInt extracts an integer query parameter. Returns defaultVal if the parameter is absent or unparseable. Does not write an error — call RequireQueryInt if the parameter is mandatory.
maxKeys := serviceutil.QueryInt(r, "max-keys", 1000)
func QueryString ¶
QueryString extracts a string query parameter, returning defaultVal if absent.
prefix := serviceutil.QueryString(r, "prefix", "")
func QueueName ¶
QueueName validates an SQS queue name. Standard queues: alphanumeric + hyphens + underscores, 1–80 chars. FIFO queues: same rules + must end in .fifo. https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-queue-message-identifiers.html
func RegionKey ¶
RegionKey builds a region-scoped store key by prepending region + "/" to key. If region is empty the key is returned unchanged, enabling "scan all regions" queries when used as a List/Scan prefix.
func RequireString ¶
RequireString checks that a string field is non-empty. On failure, writes a MissingParameter error and returns false.
if !serviceutil.RequireString(w, r, req.QueueName, "QueueName") {
return
}
func ResourceName ¶
ResourceName validates name with a reusable rule and returns the configured AWS-style error. Service-specific validators should wrap this helper rather than exposing generic resource-name policy from handlers.
func SplitRegionKey ¶
SplitRegionKey extracts the region prefix and the remaining key from a region-scoped store key. Returns ("", key) if the key has no "/" separator.
func TableName ¶
TableName validates a DynamoDB table name. Rules: 3–255 chars, alphanumeric + hyphens + underscores + periods. https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html
func ValidateAndRespond ¶
ValidateAndRespond is a convenience helper that writes the error and returns false if aerr is non-nil, otherwise returns true. Reduces boilerplate in handlers that call multiple validators in sequence.
if !serviceutil.ValidateAndRespond(w, r, serviceutil.BucketName(bucket)) {
return
}
Types ¶
type LazyInit ¶
type LazyInit struct {
// contains filtered or unexported fields
}
LazyInit provides a generic lazy-initialisation wrapper using sync.Once.
In Go, "lazy loading" of a service means: register routes immediately (so the router is ready), but defer any expensive initialisation — store setup, background goroutines, connection pools — until the first actual request arrives.
This is important for:
- Lambda: don't start Node.js process supervisors until the first Invoke
- DynamoDB: don't start the expression evaluator goroutines until first use
- Any service with expensive startup: only pay the cost if the service is called
TypeScript analogy: a module-level singleton initialised on first import, but with explicit control over when "first import" happens.
Usage in a service handler:
type Handler struct {
init serviceutil.LazyInit
// ... other fields
}
func (h *Handler) ensureInitialised(cfg *config.Config) error {
return h.init.Do(func() error {
// expensive setup: extract bootstrap.js, start supervisor, etc.
return h.startNodeSupervisor(cfg)
})
}
func (h *Handler) Invoke(w http.ResponseWriter, r *http.Request) {
if err := h.ensureInitialised(h.cfg); err != nil {
protocol.WriteJSONError(w, r, protocol.Wrap(protocol.ErrInternalError, err))
return
}
// ... handle request
}
func (*LazyInit) Do ¶
Do runs fn exactly once, the first time Do is called. Subsequent calls return the same error (or nil) without calling fn again.
If fn returns an error, the LazyInit is NOT marked as successfully done — the next call to Do will attempt fn again. This allows transient failures (e.g. "node not in PATH") to be retried without restarting the server.
If fn succeeds (returns nil), all future calls return nil immediately without calling fn again.
type NameRule ¶
type NameRule struct {
MinLength int
MaxLength int
Allowed func(rune) bool
Pattern *regexp.Regexp
ErrorCode string
LengthMessage string
AllowedMessage string
PatternMessage string
HTTPStatus int
}
NameRule describes one AWS resource-name validation rule. It intentionally carries service-specific error details because AWS does not use one global validation error shape across services.
type Page ¶
type Page[T any] struct { // Items contains the items on this page. Items []T // NextToken is the opaque continuation token to retrieve the next page. // Empty string means this is the last page. NextToken string // IsTruncated is true when there are more items after this page. // It mirrors the AWS convention (used by S3's IsTruncated field). IsTruncated bool }
Page represents a single page of results from a paginated list operation. T is the item type (e.g. *s3.Object, *sqs.Queue).
func Paginate ¶
Paginate applies limit and continuation-token logic to a full item slice, returning a Page with at most maxItems items.
The continuation token encodes the start index opaquely so that callers cannot make assumptions about the item ordering.
Example:
allObjects, _ := h.store.listObjects(ctx, bucket, prefix) page := serviceutil.Paginate(allObjects, maxKeys, req.ContinuationToken) // page.Items — items for this page // page.NextToken — pass back to client as NextContinuationToken // page.IsTruncated — set in the response envelope
type ServiceLogger ¶
type ServiceLogger struct {
// contains filtered or unexported fields
}
ServiceLogger wraps a zap.Logger with service-scoped context. All log calls automatically include the service name as a structured field, so log lines are filterable by service without adding the field at every call.
Create one per service in service.go and pass it to the handler:
type Service struct {
log *serviceutil.ServiceLogger
...
}
func New(cfg *config.Config, store state.Store, logger *zap.Logger) *Service {
return &Service{
log: serviceutil.NewServiceLogger(logger, "s3"),
...
}
}
func NewServiceLogger ¶
func NewServiceLogger(logger *zap.Logger, service string) *ServiceLogger
NewServiceLogger returns a ServiceLogger scoped to the named service. When stdout is an interactive terminal, log messages are prefixed with a coloured [SERVICE] tag so different services are instantly distinguishable when running locally. In non-interactive environments (CI, Docker) the tag is omitted and the structured "service" field carries the same information.
func (*ServiceLogger) Debug ¶
func (l *ServiceLogger) Debug(msg string, fields ...zap.Field)
Debug logs at DEBUG level — fine-grained detail useful during development. Use for: parsed request parameters, individual state reads/writes. Do NOT use for: every middleware step (already logged by Logger middleware).
func (*ServiceLogger) Error ¶
func (l *ServiceLogger) Error(msg string, fields ...zap.Field)
Error logs at ERROR level — failures producing 5xx responses. Use for: state backend failures, serialisation errors, panics. Always include zap.Error(err) — never log just the message without the error.
func (*ServiceLogger) Info ¶
func (l *ServiceLogger) Info(msg string, fields ...zap.Field)
Info logs at INFO level — significant lifecycle events. Use for: resource created/deleted, queue purged, bucket emptied. Do NOT use for: individual requests (already logged by Logger middleware).
func (*ServiceLogger) LogStateError ¶
func (l *ServiceLogger) LogStateError(r *http.Request, op string, aerr *protocol.AWSError, fields ...zap.Field)
LogStateError logs a state backend failure at ERROR level with standard fields. This is the canonical way to log storage failures in service store.go files:
if aerr := s.store.putObject(ctx, obj); aerr != nil {
s.log.LogStateError(r, "put object", aerr, zap.String("bucket", bucket), zap.String("key", key))
protocol.WriteXMLError(w, r, aerr)
return
}
func (*ServiceLogger) Logger ¶
func (l *ServiceLogger) Logger() *zap.Logger
Logger returns the underlying zap.Logger for cases where raw zap is needed.
func (*ServiceLogger) Warn ¶
func (l *ServiceLogger) Warn(msg string, fields ...zap.Field)
Warn logs at WARN level — handled but unexpected conditions. Use for: oversized payloads, deprecated parameters, known limitations hit.
func (*ServiceLogger) With ¶
func (l *ServiceLogger) With(fields ...zap.Field) *ServiceLogger
With returns a new ServiceLogger with the additional fields attached to every subsequent log call. Useful for adding operation-scoped context:
log := h.log.With(zap.String("bucket", bucket), zap.String("key", key))
log.Debug("fetching object")
func (*ServiceLogger) WithOperation ¶
func (l *ServiceLogger) WithOperation(op string) *ServiceLogger
WithOperation returns a child ServiceLogger scoped to the named operation (e.g. "CreateQueue", "PutObject"). All log calls on the returned logger automatically include an "operation" structured field.
In console mode, messages are also prefixed with a dim [Operation] tag so it is easy to trace which handler produced each log line:
10:42:03 DEBUG [S3] [PutObject] stored object {bucket=foo key=bar}
Typical usage at the top of a handler:
log := h.log.WithOperation("PutObject")
log.Debug("decoded request", zap.String("bucket", bucket))
func (*ServiceLogger) ZapLogger ¶
func (l *ServiceLogger) ZapLogger() *zap.Logger
ZapLogger returns the underlying *zap.Logger so callers that require the raw logger (e.g. Docker GC) can create named children from the service-scoped logger.