Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e8aeadc
docs(gitflow): release branches follow semver, without v
jpfaria Oct 19, 2026
c717ec5
Merge pull request #219 from xgodev/bugfix/1.6.2
jpfaria Oct 19, 2026
582f0a0
docs(gitflow): match the flow the repo follows (#220)
jpfaria Oct 19, 2026
6063140
Merge pull request #221 from xgodev/bugfix/220-gitflow
jpfaria Oct 19, 2026
92ac260
docs(gitflow): only the tag carries v (#222)
jpfaria Oct 19, 2026
020d40a
Merge pull request #223 from xgodev/bugfix/222-gitflow-no-v
jpfaria Oct 19, 2026
044e6c6
fix(examples): regenerate the function examples (#225)
jpfaria Oct 19, 2026
094bfa5
Merge pull request #226 from xgodev/bugfix/225-regenerate-function-ex…
jpfaria Oct 19, 2026
19cccb5
feat(examples): add the complete stock app (#227)
jpfaria Oct 19, 2026
250aaa2
Merge pull request #229 from xgodev/feature/227-example-app
jpfaria Oct 19, 2026
0373271
fix(function): log the port the cloudevents http adapter listens on (…
jpfaria Oct 19, 2026
08f0166
Merge pull request #231 from xgodev/bugfix/230-cloudevents-port-log
jpfaria Oct 19, 2026
9e135a8
fix(grpc): answer a Conflict with Aborted, not AlreadyExists (#232)
jpfaria Oct 19, 2026
ebda118
Merge pull request #233 from xgodev/bugfix/232-grpc-conflict-aborted
jpfaria Oct 19, 2026
62f7670
fix(inject): close the mongo and redis clients when the app stops (#234)
jpfaria Oct 19, 2026
2e8a320
Merge pull request #235 from xgodev/bugfix/234-close-mongo-redis-clients
jpfaria Oct 19, 2026
46f5ec5
docs(site): take the shop mongo conn through @Inject (#236)
jpfaria Oct 19, 2026
9256194
Merge pull request #237 from xgodev/bugfix/236-site-shop-inject-mongo
jpfaria Oct 19, 2026
dee03f8
fix(fx): give the redis client and cluster modules a sync.Once each (…
jpfaria Oct 19, 2026
b041fff
Merge pull request #239 from xgodev/bugfix/238-redis-modules-once
jpfaria Oct 19, 2026
15e2ff6
fix(examples): say once that an unknown sku is not in stock (#240)
jpfaria Oct 20, 2026
a0b3082
Merge pull request #241 from xgodev/bugfix/240-stock-404-message
jpfaria Oct 20, 2026
b07c785
feat(builder): accept kebab-case app names (#242)
jpfaria Oct 20, 2026
8010412
Merge pull request #245 from xgodev/feature/242-kebab-app-names
jpfaria Oct 20, 2026
9947054
feat(builder): @Job door, an app that runs one function and exits (#243)
jpfaria Oct 20, 2026
6666192
Merge pull request #248 from xgodev/feature/243-job-door
jpfaria Oct 20, 2026
3a082ce
feat(inject): @Inject builds *sql.DB through the godror and pgx facto…
jpfaria Oct 20, 2026
bce6edf
Merge pull request #249 from xgodev/feature/244-inject-sql-db
jpfaria Oct 20, 2026
310c0fd
feat(builder): @Server picks the plugins of each server type (#247)
jpfaria Oct 20, 2026
c81c83b
Merge pull request #250 from xgodev/feature/247-server-annotation
jpfaria Oct 20, 2026
f325f01
docs(examples): README of examples/stock with runs, calls, config and…
jpfaria Oct 20, 2026
3e3e315
Merge pull request #251 from xgodev/feature/246-stock-readme
jpfaria Oct 20, 2026
56b37dd
docs(claude): every change updates docs, site and asks claude-plugin …
jpfaria Oct 20, 2026
0da522b
Merge pull request #253 from xgodev/feature/252-docs-premise
jpfaria Oct 20, 2026
fb54d0e
release: 1.7.0
jpfaria Oct 20, 2026
935ea02
fix(examples): check the sku before the stock and match it with $eq (…
jpfaria Oct 20, 2026
411b44d
Merge pull request #258 from xgodev/bugfix/255-sku-filter
jpfaria Oct 20, 2026
3fd108f
Merge remote-tracking branch 'origin/develop' into release/1.7.0
jpfaria Oct 20, 2026
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
24 changes: 18 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ factory por componente sob `factory/contrib/`.
- API pública alterada sem teste e sem nota de compat
- `git push` sem quality-gate verde (suspenso por enquanto, ver `docs/development/quality-gate.md`)
- Issue ou PR ausentes (ver `docs/development/gitflow.md`)
- Mudança sem README/site atualizados ou sem issue no `xgodev/claude-plugin`
- Tag, GitHub Release, PR `develop` → `main` ou bump de versão do site/README
sem o mantenedor mandar lançar. Issue para no merge em `develop`; release
só por `release/vX.Y.Z` (ver `docs/development/gitflow.md`, seção Release)
só por `release/X.Y.Z` (minor/major) ou `bugfix/X.Y.Z` / `hotfix/X.Y.Z`
(patch), sem `v` no nome da branch (ver `docs/development/gitflow.md`, seção Versão)

## Regras gerais de código

Expand All @@ -35,8 +37,16 @@ factory por componente sob `factory/contrib/`.
- **Single source of truth** — config keys e defaults declarados uma vez no
`config.go` do componente.
- **Separação de concerns** — factory constrói, wrapper abstrai, app consome.
- Documentação é parte da tarefa: mudou componente/config key/comportamento →
atualizar o README do pacote no mesmo commit.
- **Premissa: toda mudança leva a doc junto.** Coisa nova, alterada ou
removida (componente, config key, anotação, comportamento) → no mesmo PR,
o README do pacote, o `README.md` da raiz e o site quando eles mostram
aquilo; e uma issue no `xgodev/claude-plugin` pedindo a atualização da
skill do boost (ver [Doc do plugin](#doc-do-plugin-fora-deste-repo)).
Sem as três, a issue não está pronta.
- **`examples/stock` é o app de exemplo completo.** Recurso novo no boost
(factory, plugin, adapter, anotação do builder) entra nele no mesmo PR,
com teste em `cmd/<app>/main_test.go` e linha no `examples/stock/README.md`
(chamada com a resposta real, chave de config, arquivo do recurso).
- **`README.md` da raiz espelha o site** (`docs/site/index.html`): mesmas
seções, texto, números, exemplo e versão. Mudou um, muda o outro no mesmo PR.
- Teste valida comportamento, não só cobre linha. Bug fix → teste vermelho
Expand All @@ -46,9 +56,11 @@ factory por componente sob `factory/contrib/`.

A doc de consumo do boost e o Quality Gate são distribuídos pelo plugin
**`claude-plugin@xgodev`** (repo `xgodev/claude-plugin`). Este repo (`boost`)
**não é marketplace, não contém skills do plugin e não sincroniza doc de
plugin** — isso é responsabilidade do `xgodev/claude-plugin`, fora do escopo
daqui.
**não é marketplace, não contém skills do plugin e não edita o plugin.**
Toda mudança no boost abre uma issue no `xgodev/claude-plugin` (label
`documentation`) dizendo o que mudou, com link para a issue/PR do boost e
para o README que tem os detalhes; quem atualiza a skill é o
`xgodev/claude-plugin`.

Instalação do plugin:

Expand Down
131 changes: 99 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ github.com/acme/shop
├── doc.go go:generate
└── internal/
├── app/api/rest/
│ ├── handler/stock/reserver/handler.go @REST
│ └── plugins.go @Provide
│ └── handler/stock/reserver/handler.go @REST
└── pkg/
├── server/rest/doc.go @Server
├── domain/
│ ├── service/stock/reserver/
│ │ ├── local/service.go @Middleware
Expand All @@ -56,7 +56,7 @@ github.com/acme/shop
├── memory/datastore.go @Default
└── mongodb/
├── config.go @Config
└── datastore.go @Provide
└── datastore.go @Inject
```

`internal/app/api/rest/handler/stock/reserver/handler.go`
Expand Down Expand Up @@ -94,23 +94,14 @@ func (h *Handler) Handle(c echo.Context) error {
```

<details>
<summary><code>internal/app/api/rest/plugins.go</code></summary>
<summary><code>internal/pkg/server/rest/doc.go</code></summary>

```go
// Package rest holds what the rest server of app api needs besides its
// handlers.
// handlers: errorHandler turns boost errors into HTTP statuses, so a
// Conflict answers 409.
// @Server(type=rest, plugins=[recover, errorHandler])
package rest

import (
boostecho "github.com/xgodev/boost/factory/contrib/labstack/echo/v4"
"github.com/xgodev/boost/factory/contrib/labstack/echo/v4/plugins/extra/error_handler"
)

// Plugins turns boost errors into HTTP statuses: a Conflict answers 409.
// @Provide(index=0)
func Plugins() []boostecho.Plugin {
return []boostecho.Plugin{error_handler.Register}
}
```

</details>
Expand Down Expand Up @@ -258,19 +249,17 @@ type datastore struct {
coll *mongo.Collection
}

// New connects through boost's mongo factory (boost.factory.mongo.*).
// New keeps the stock on the conn inject builds from boost.factory.mongo.*,
// which the app closes when it stops.
// @Provide(index=0)
func New(ctx context.Context, o *Options) (stock.Repository, error) {
conn, err := mongofact.NewConn(ctx)
if err != nil {
return nil, err
}
return &datastore{coll: conn.Database.Collection(o.Collection)}, nil
// @Inject(index=0, plugins=[health, prometheus])
func New(conn *mongofact.Conn, o *Options) stock.Repository {
return &datastore{coll: conn.Database.Collection(o.Collection)}
}

func (d *datastore) Take(ctx context.Context, sku string) error {
res, err := d.coll.UpdateOne(ctx,
bson.M{"_id": sku, "units": bson.M{"$gt": 0}},
bson.M{"_id": bson.M{"$eq": sku}, "units": bson.M{"$gt": 0}},
bson.M{"$inc": bson.M{"units": -1}})
if err != nil {
return err
Expand Down Expand Up @@ -307,12 +296,13 @@ gen/
│ │ ├── modules.go app api
│ │ └── rest_module.go @REST
│ └── middleware/…/local/new_middleware.go @Middleware
├── client/…/mongo-driver/v2/
│ └── default_conn.go @Inject
├── implementation/…/stock/
│ └── repository_implementation.go @Default
├── internal/
│ ├── app/api/rest/
│ │ ├── handler/stock/reserver/new_module.go
│ │ └── plugins_module.go
│ │ └── handler/stock/reserver/new_module.go
│ └── pkg/
│ ├── domain/service/…/local/new_module.go
│ └── datastore/stock/
Expand Down Expand Up @@ -387,10 +377,11 @@ import (

"go.uber.org/fx"

gen_rest_54b9e997 "github.com/acme/shop/gen/internal/app/api/rest"
gen_reserver_5a8b66f7 "github.com/acme/shop/gen/internal/app/api/rest/handler/stock/reserver"
src_reserver_5a8b66f7 "github.com/acme/shop/internal/app/api/rest/handler/stock/reserver"
boostecho "github.com/xgodev/boost/factory/contrib/labstack/echo/v4"
echoerrorhandler "github.com/xgodev/boost/factory/contrib/labstack/echo/v4/plugins/extra/error_handler"
echorecover "github.com/xgodev/boost/factory/contrib/labstack/echo/v4/plugins/native/recover"
echofx "github.com/xgodev/boost/fx/modules/factory/contrib/labstack/echo/v4"
)

Expand All @@ -404,8 +395,13 @@ func RestModule() fx.Option {
restOnce.Do(func() {
options = fx.Module("api.rest",
echofx.Module(),
fx.Provide(func() []boostecho.Plugin {
return []boostecho.Plugin{
echorecover.Register,
echoerrorhandler.Register,
}
}),
gen_reserver_5a8b66f7.NewModule(),
gen_rest_54b9e997.PluginsModule(),
fx.Invoke(func(srv *boostecho.Server, h0 *src_reserver_5a8b66f7.Handler) {
srv.Add("POST", "/stock/:sku/reserve", h0.Handle)
}),
Expand Down Expand Up @@ -485,6 +481,75 @@ func (d *newDecorator) Reserve(ctx context.Context, a1 string) error {

</details>

<details>
<summary><code>gen/client/github.com/xgodev/boost/factory/contrib/go.mongodb.org/mongo-driver/v2/default_conn.go</code></summary>

```go
// Code generated by inject; DO NOT EDIT.

package mongo

import (
"context"
"sync"

"go.uber.org/fx"

factory "github.com/xgodev/boost/factory/contrib/go.mongodb.org/mongo-driver/v2"
prometheusplugin "github.com/xgodev/boost/factory/contrib/go.mongodb.org/mongo-driver/v2/plugins/contrib/prometheus/client_golang/v1"
healthplugin "github.com/xgodev/boost/factory/contrib/go.mongodb.org/mongo-driver/v2/plugins/local/health"
contextfx "github.com/xgodev/boost/fx/modules/core/context"
)

// defaultConnRoot is the config root of the default client (*github.com/xgodev/boost/factory/contrib/go.mongodb.org/mongo-driver/v2.Conn);
// env BOOST_FACTORY_MONGO_*.
const defaultConnRoot = "boost.factory.mongo"

func init() {
factory.ConfigAdd(defaultConnRoot)
healthplugin.ConfigAdd(defaultConnRoot + ".plugins.health")
prometheusplugin.ConfigAdd(defaultConnRoot + ".plugins.prometheus")
}

var defaultConnOnce sync.Once

// DefaultConnModule provides the default client (*github.com/xgodev/boost/factory/contrib/go.mongodb.org/mongo-driver/v2.Conn), built from the keys under
// boost.factory.mongo with the plugins health, prometheus, and closes it when the app stops.
// It returns an empty option after the first call, so every consumer can
// include it.
func DefaultConnModule() fx.Option {
options := fx.Options()

defaultConnOnce.Do(func() {
options = fx.Module("github.com/xgodev/boost/factory/contrib/go.mongodb.org/mongo-driver/v2.DefaultConn",
contextfx.Module(),
fx.Provide(newDefaultConn),
)
})

return options
}

func newDefaultConn(ctx context.Context, lc fx.Lifecycle) (*factory.Conn, error) {
health, err := healthplugin.NewHealthWithConfigPath(defaultConnRoot + ".plugins.health")
if err != nil {
return nil, err
}
prometheus, err := prometheusplugin.NewPrometheusWithConfigPath(defaultConnRoot + ".plugins.prometheus")
if err != nil {
return nil, err
}
c, err := factory.NewConnWithConfigPath(ctx, defaultConnRoot, health.Register, prometheus.Register)
if err != nil {
return nil, err
}
lc.Append(fx.StopHook(c.Close))
return c, nil
}
```

</details>

<details>
<summary><code>gen/implementation/internal_/pkg/domain/repository/stock/repository_implementation.go</code></summary>

Expand Down Expand Up @@ -600,7 +665,7 @@ The other generated files are in
```console
$ go generate ./...
5:35PM INF scanning ~/shop (module github.com/acme/shop)
5:35PM INF generated 6 inject modules and 1 apps in ~/shop/gen, mains in ~/shop/cmd
5:35PM INF generated 5 inject modules and 1 apps in ~/shop/gen, mains in ~/shop/cmd
$ go run ./cmd/api &
5:36PM INF starting Echo Server. https://echo.labstack.com/
# memory is the @Default datastore: 3 t-shirts in stock
Expand Down Expand Up @@ -760,8 +825,10 @@ the app modules and main.
| `@GRPC` | gRPC service |
| `@Function` | azure functions · cloudevents · kafka · kafka-go · lambda · nats · pubsub |
| `@MCP` | MCP tool |
| `@Job` | runs once and exits · CronJob |
| `@Middleware` | generated decorator |
| `@Default` | implementation picked by config |
| `@Server` | plugins of a server |

### Functions

Expand Down Expand Up @@ -804,7 +871,7 @@ services is generated, checked and rewritten on every run.

1. **Annotate.** Mark constructors with `@Provide`, handlers with a door,
services with `@Middleware`, the default implementation with `@Default`,
options with `@Config`.
options with `@Config`, the plugins of a server with `@Server`.
2. **Generate.** Inject and builder run in one pass. A broken graph stops here
and leaves your output untouched.

Expand Down Expand Up @@ -869,7 +936,7 @@ same or get better, never worse.

require (
- github.com/xgodev/boost v1.1.1
+ github.com/xgodev/boost v1.6.2
+ github.com/xgodev/boost v1.7.0
)
```

Expand All @@ -880,7 +947,7 @@ ok github.com/acme/shop/...

1 line changed in go.mod, **0 lines changed in your code**.

Releases: 2024-07 → `v1.1.0` → `v1.1.1` → **`v1.6.2`**
Releases: 2024-07 → `v1.1.0` → `v1.1.1` → **`v1.7.0`**
([all releases](https://github.com/xgodev/boost/releases)).

## There's a factory for that.
Expand Down
6 changes: 5 additions & 1 deletion annotation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,14 @@ for _, entry := range collector.EntriesWith("RestRouter") {
|---|---|
| `WithPath(dir)` | Scans the packages under `dir` as `go list ./...` sees them from there: hidden (`.`), `_`, `testdata` and `vendor` directories are skipped. The packages they import are scanned too. |
| `WithFilters(names...)` | Keeps only annotations whose name starts with one of the filters: `Rest` keeps `@Rest`, `@RestRouter` and `@RestResponse`. Without it, every annotation is kept. |
| `WithPackageDocs()` | Also collects the annotations of package doc comments: one entry per file whose package doc has one. Without it, only declarations are collected. |
| `WithPackages(paths...)` | Scans only these import paths and the packages below them, both under the path and among their imports. The imports of a package that is not scanned are not followed. Without it, every non-standard-library package reached is scanned. |

Standard library packages are never scanned.

### Entries

Each `Entry` is an annotated struct type, method or function:
Each `Entry` is an annotated struct type, method, function or package doc:

- `IsStruct()`: a struct type. Interfaces and other non-struct types are not reported.
`Fields` lists its fields in order, one per name (`A, B string` is two), with
Expand All @@ -75,6 +76,9 @@ Each `Entry` is an annotated struct type, method or function:
- `IsMethod()`: a method, with `Struct` set to its receiver type (`T`, `*T` or a
generic `T[K]`), wherever in the package the type is declared.
- `IsFunc()`: a package-level function.
- `IsPackage()`: the package doc comment of a file (`// Package x ...` above
`package x`), only with `WithPackageDocs()`. `Struct` and `Func` are empty and
`Header.Title` drops the leading `Package x`.

`Path` is the import path, `Package` the package name, `Module` the module path
and `File` the base name of the file that declares it. `Header.Title` is the first
Expand Down
10 changes: 10 additions & 0 deletions annotation/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,16 @@ func WithPackages(pkgs ...string) Option {
}
}

// WithPackageDocs also collects the annotations of package doc comments,
// each file's as an entry whose IsPackage is true. Without it, only
// declarations are collected.
func WithPackageDocs() Option {
return func(c *Collector) error {
c.packageDocs = true
return nil
}
}

// WithPath sets the directory to scan. It must be inside a Go module.
func WithPath(path string) Option {
return func(c *Collector) error {
Expand Down
22 changes: 22 additions & 0 deletions annotation/collect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ import (

// @Rest(method=PUT)
func (s *Service) Put() { _ = strings.ToUpper(d2.Hello()); Sprint() }
`,
"app/doc.go": `// Package app serves the fixture.
// @Boost(kind=pkg)
// @Other
package app
`,
"app/options.go": `package app

Expand Down Expand Up @@ -217,6 +222,23 @@ func TestCollect_StructFields(t *testing.T) {
assert.Empty(t, find(entries, "Service", "")[0].Fields, "an empty struct has no fields")
}

func TestCollect_PackageDocsOnlyWhenAsked(t *testing.T) {
root := fixtureModule(t)

assert.Empty(t, find(collectEntries(t, WithPath(root)), "", ""), "package docs are left out by default")

pkg := find(collectEntries(t, WithPath(root), WithPackageDocs()), "", "")
require.Len(t, pkg, 1)
assert.True(t, pkg[0].IsPackage())
assert.False(t, pkg[0].IsFunc() || pkg[0].IsStruct() || pkg[0].IsMethod())
assert.Equal(t, "example.com/fixture/app", pkg[0].Path)
assert.Equal(t, "app", pkg[0].Package)
assert.Equal(t, "doc.go", pkg[0].File)
assert.Equal(t, "serves the fixture.", pkg[0].Header.Title, "the title drops the leading \"Package app\"")
require.Len(t, pkg[0].Annotations, 1, "the filters apply to package docs too")
assert.Equal(t, "pkg", pkg[0].Annotations[0].Map["kind"])
}

func TestCollect_FiltersMatchTheAnnotationName(t *testing.T) {
entries := collectEntries(t, WithPath(fixtureModule(t)))

Expand Down
Loading
Loading