serviceutil

package
v0.0.1-alpha.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 12, 2026 License: MIT Imports: 15 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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

func ARNRegion(arn string) string

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

func AlphaNumericHyphenUnderscorePeriod(c rune) bool

AlphaNumericHyphenUnderscorePeriod matches the common AWS identifier alphabet used by SQS queue names and DynamoDB table names.

func AppSyncDataSourceName

func AppSyncDataSourceName(name string) *protocol.AWSError

func AppSyncFieldName

func AppSyncFieldName(name string) *protocol.AWSError

func AppSyncFunctionName

func AppSyncFunctionName(name string) *protocol.AWSError

func AppSyncGraphQLAPIName

func AppSyncGraphQLAPIName(name string) *protocol.AWSError

AppSyncGraphQLAPIName validates the required GraphQL API name. AWS documents this field as required without publishing a stricter length or pattern.

func AppSyncIdentifierName

func AppSyncIdentifierName(name, field string) *protocol.AWSError

AppSyncIdentifierName validates AppSync names documented with the [_A-Za-z][_0-9A-Za-z]* pattern and 1..65536 length constraint.

func AppSyncTypeName

func AppSyncTypeName(name string) *protocol.AWSError

func BucketName

func BucketName(name string) *protocol.AWSError

BucketName validates an S3 bucket name against AWS naming rules. Returns nil if valid, or a *protocol.AWSError with code "InvalidBucketName".

func ClampInt

func ClampInt(v, min, max int) int

ClampInt returns v clamped to [min, max].

maxMessages := serviceutil.ClampInt(req.MaxNumberOfMessages, 1, 10)

func DecodeJSON

func DecodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool

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

func DefaultInt(v, defaultVal int) int

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

func HasQueryParam(r *http.Request, param string) bool

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

func HeaderPrefix(r *http.Request, prefix string) map[string]string

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

func ParseIntDefault(s string, defaultVal int) int

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

func QueryInt(r *http.Request, param string, defaultVal int) int

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

func QueryString(r *http.Request, param, defaultVal string) string

QueryString extracts a string query parameter, returning defaultVal if absent.

prefix := serviceutil.QueryString(r, "prefix", "")

func QueueName

func QueueName(name string) *protocol.AWSError

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

func RegionKey(region, key string) string

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

func RequireString(w http.ResponseWriter, r *http.Request, value, paramName string) bool

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

func ResourceName(name string, rule NameRule) *protocol.AWSError

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

func SplitRegionKey(key string) (region, rest string)

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

func TableName(name string) *protocol.AWSError

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

func ValidateAndRespond(w http.ResponseWriter, r *http.Request, aerr *protocol.AWSError) bool

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

func (l *LazyInit) Do(fn func() error) error

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.

func (*LazyInit) Done

func (l *LazyInit) Done() bool

Done reports whether the lazy initialisation has completed successfully.

func (*LazyInit) Reset

func (l *LazyInit) Reset()

Reset forces re-initialisation on the next Do call. Primarily useful in tests that need a clean state between runs.

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

func Paginate[T any](items []T, maxItems int, continuationToken string) Page[T]

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL