Add a command
Model a real yelp record and expose it as a command, a route, and a tool at once.
yelp already models businesses, reviews, users, suggestions, and categories.
Adding another surface means modelling one more record and declaring its
operation. You do that in two files, and every surface updates itself.
1. Model the record
In the yelp package, add a struct for the thing you are fetching and a client
method that returns it. The kit struct tags decide how a host addresses the
record:
type Item struct {
ID string `json:"id" kit:"id"` // the URI id
Title string `json:"title"`
Body string `json:"body" kit:"body"` // what cat and Markdown print
Owner string `json:"owner" kit:"link,kind=yelp/user"` // an edge to another record
}
func (c *Client) GetItem(ctx context.Context, id string) (*Item, error) {
body, err := c.Get(ctx, BaseURL+"/items/"+id+".json")
if err != nil {
return nil, err
}
// decode body into an Item ...
return item, nil
}
kit:"id"marks the field that becomes the URI id.kit:"body"marks the prose thatcatand the Markdown export render.kit:"link,kind=<scheme>/<type>"marks an outbound edge. It can point at another yelp type or at another site entirely, which is what lets a host walk the graph across tools.
2. Declare the operation
In yelp/domain.go, add an input struct and a handler, then register
it in Register:
type itemRef struct {
Ref string `kit:"arg" help:"item id or URL"`
Client *Client `kit:"inject"`
}
func getItem(ctx context.Context, in itemRef, emit func(*Item) error) error {
it, err := in.Client.GetItem(ctx, in.Ref)
if err != nil {
return mapErr(err)
}
return emit(it)
}
// inside Register(app):
kit.Handle(app, kit.OpMeta{Name: "item", Group: "read", Single: true,
Summary: "Fetch an item by id or URL", URIType: "item", Resolver: true,
Args: []kit.Arg{{Name: "ref", Help: "item id or URL"}}}, getItem)
That is the whole change. kit.Handle reflects the input for flags and the
output for the record shape, so the operation immediately becomes:
yelp item <id> # the command
curl 'localhost:7777/v1/item/<id>' # the route, under serve
ant get yelp://item/<id> # the URI dereference, via a host
Resolver ops and list ops
Two flags shape how a host treats an operation:
Single: truewithResolver: truemarks the canonical one-record fetch for aURIType. It answersant get.List: truemarks a member-lister for a parent resource. It answersant ls. A list op should emit records that are themselves addressable (often a lightweight stub of a resolver type), so every member is a URI a host can follow. Thereviewsop does this: each review carries an edge back to its business and on to the reviewer's profile.
Map errors to exit codes
Return the errs kinds from mapErr so every surface reports the same outcome
with the same exit code:
case errors.Is(err, ErrNotFound):
return errs.NotFound("%s", err.Error())
case errors.Is(err, ErrRateLimited):
return errs.RateLimited("%s", err.Error())
See output formats for how records render, and resource URIs for how a host addresses them.