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
21 changes: 9 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ go install github.com/mhiro2/seedling/cmd/seedling-gen@latest
seedling-gen sql --explain schema.sql
```

This generates struct types, `RegisterBlueprints()`, deterministic `Defaults` for common scalar fields, relations, and Insert stubs. Fill in the `// TODO` callbacks with your DB logic:
This generates struct types, `NewRegistry()`, `RegisterBlueprints(reg)`, deterministic `Defaults` for common scalar fields, relations, and Insert stubs. Fill in the `// TODO` callbacks with your DB logic:

```go
Insert: func(ctx context.Context, db seedling.DBTX, v Company) (Company, error) {
Expand All @@ -127,10 +127,9 @@ go install github.com/mhiro2/seedling/cmd/seedling-gen@latest

```go
func TestUser(t *testing.T) {
seedling.ResetRegistry()
testutil.RegisterBlueprints()
reg := testutil.NewRegistry()

result := seedling.InsertOne[testutil.User](t, db)
result := seedling.NewSession[testutil.User](reg).InsertOne(t, db)
user := result.Root()

if user.ID == 0 {
Expand All @@ -146,12 +145,11 @@ go install github.com/mhiro2/seedling/cmd/seedling-gen@latest

```go
func TestNamedUser(t *testing.T) {
seedling.ResetRegistry()
testutil.RegisterBlueprints()
reg := testutil.NewRegistry()

company := seedling.InsertOne[testutil.Company](t, db).Root()
company := seedling.NewSession[testutil.Company](reg).InsertOne(t, db).Root()

result := seedling.InsertOne[testutil.User](t, db,
result := seedling.NewSession[testutil.User](reg).InsertOne(t, db,
seedling.Set("Name", "alice"),
seedling.Use("company", company),
)
Expand All @@ -161,12 +159,11 @@ go install github.com/mhiro2/seedling/cmd/seedling-gen@latest
}

func TestTaskProject(t *testing.T) {
seedling.ResetRegistry()
testutil.RegisterBlueprints()
reg := testutil.NewRegistry()

// Only("project") inserts task + project subtree only,
// skipping the assignee relation entirely.
result := seedling.InsertOne[testutil.Task](t, db,
result := seedling.NewSession[testutil.Task](reg).InsertOne(t, db,
seedling.Only("project"),
)
_ = result
Expand All @@ -191,7 +188,7 @@ go install github.com/mhiro2/seedling/cmd/seedling-gen@latest
## 📂 Examples

- [basic](./examples/basic): register blueprints and insert rows with automatic parent creation
- [quickstart](./examples/quickstart): generated-style `RegisterBlueprints()` flow that matches the README Quick Start
- [quickstart](./examples/quickstart): generated-style `NewRegistry()` / `RegisterBlueprints(reg)` flow that matches the README Quick Start
- [custom-defaults](./examples/custom-defaults): customize values with `Set`, `With`, and `Generate`
- [reuse-parent](./examples/reuse-parent): reuse existing rows with `Use`
- [batch-insert](./examples/batch-insert): batch inserts with shared `Ref` dependencies and per-row `SeqRef` overrides
Expand Down
8 changes: 7 additions & 1 deletion cmd/seedling-gen/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ func TestGenerate_OutputIncludesExpectedSections(t *testing.T) {
{name: "context import", substr: `"context"`, message: "output should import context"},
{name: "time import", substr: `"time"`, message: "output should import time"},
{name: "seedling import", substr: `"github.com/mhiro2/seedling"`, message: "output should import seedling"},
{name: "new registry helper", substr: "func NewRegistry() *seedling.Registry", message: "output should contain local registry helper"},
{name: "registry argument", substr: "func RegisterBlueprints(reg *seedling.Registry)", message: "output should accept a registry argument"},
{name: "register to registry", substr: "seedling.MustRegisterTo(reg", message: "output should register into the provided registry"},
{name: "company struct", substr: "type Company struct", message: "output should contain Company struct"},
{name: "user struct", substr: "type User struct", message: "output should contain User struct"},
{name: "company blueprint", substr: `Name: "company"`, message: "output should register company blueprint"},
Expand Down Expand Up @@ -315,7 +318,10 @@ func TestGenerate_EmptyInput(t *testing.T) {
if !strings.Contains(output, "package empty") {
t.Fatalf("expected package declaration, got:\n%s", output)
}
if !strings.Contains(output, "func RegisterBlueprints()") {
if !strings.Contains(output, "func NewRegistry() *seedling.Registry") {
t.Fatalf("expected NewRegistry function, got:\n%s", output)
}
if !strings.Contains(output, "func RegisterBlueprints(reg *seedling.Registry)") {
t.Fatalf("expected RegisterBlueprints function, got:\n%s", output)
}
}
Expand Down
10 changes: 8 additions & 2 deletions cmd/seedling-gen/normalized_ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,17 @@ type {{.StructName}} struct {
`

const normalizedBlueprintTemplate = `
func RegisterBlueprints() {
func NewRegistry() *seedling.Registry {
reg := seedling.NewRegistry()
RegisterBlueprints(reg)
return reg
}

func RegisterBlueprints(reg *seedling.Registry) {
{{- range $i, $model := .}}
{{- if $i}}
{{ end }}
seedling.MustRegister(seedling.Blueprint[{{$model.TypeExpr}}]{
seedling.MustRegisterTo(reg, seedling.Blueprint[{{$model.TypeExpr}}]{
Name: "{{$model.BlueprintID}}",
Table: "{{$model.TableName}}",
{{- if isCompositePK $model}}
Expand Down
59 changes: 32 additions & 27 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,40 +52,45 @@
//
// Register a [Blueprint] for each model that seedling should create:
//
// seedling.MustRegister(seedling.Blueprint[Company]{
// Name: "company",
// Table: "companies",
// PKField: "ID",
// Defaults: func() Company {
// return Company{Name: "test-company"}
// },
// Insert: func(ctx context.Context, db seedling.DBTX, v Company) (Company, error) {
// return insertCompany(ctx, db, v)
// },
// })
//
// seedling.MustRegister(seedling.Blueprint[User]{
// Name: "user",
// Table: "users",
// PKField: "ID",
// Defaults: func() User {
// return User{Name: "test-user"}
// },
// Relations: []seedling.Relation{
// {Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
// },
// Insert: func(ctx context.Context, db seedling.DBTX, v User) (User, error) {
// return insertUser(ctx, db, v)
// },
// })
// func registerBlueprints(reg *seedling.Registry) {
// seedling.MustRegisterTo(reg, seedling.Blueprint[Company]{
// Name: "company",
// Table: "companies",
// PKField: "ID",
// Defaults: func() Company {
// return Company{Name: "test-company"}
// },
// Insert: func(ctx context.Context, db seedling.DBTX, v Company) (Company, error) {
// return insertCompany(ctx, db, v)
// },
// })
//
// seedling.MustRegisterTo(reg, seedling.Blueprint[User]{
// Name: "user",
// Table: "users",
// PKField: "ID",
// Defaults: func() User {
// return User{Name: "test-user"}
// },
// Relations: []seedling.Relation{
// {Name: "company", Kind: seedling.BelongsTo, LocalField: "CompanyID", RefBlueprint: "company"},
// },
// Insert: func(ctx context.Context, db seedling.DBTX, v User) (User, error) {
// return insertUser(ctx, db, v)
// },
// })
// }
//
// DBTX is intentionally opaque. Your insert callback and your call sites must
// agree on the concrete handle type passed as db.
//
// Then create rows directly in your tests:
//
// func TestUser(t *testing.T) {
// result := seedling.InsertOne[User](t, db)
// reg := seedling.NewRegistry()
// registerBlueprints(reg)
//
// result := seedling.NewSession[User](reg).InsertOne(t, db)
// user := result.Root()
// // user.ID and user.CompanyID are populated.
// }
Expand Down
2 changes: 1 addition & 1 deletion docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ Supported locales: `en` (default), `ja`, `zh`, `ko`, `de`, `fr`.
## Examples

- [basic](../examples/basic) -- register blueprints and insert rows with automatic parent creation
- [quickstart](../examples/quickstart) -- generated-style `RegisterBlueprints()` flow that matches the README Quick Start
- [quickstart](../examples/quickstart) -- generated-style `NewRegistry()` / `RegisterBlueprints(reg)` flow that matches the README Quick Start
- [custom-defaults](../examples/custom-defaults) -- customize values with `Set`, `With`, and `Generate`
- [reuse-parent](../examples/reuse-parent) -- reuse existing rows with `Use`
- [batch-insert](../examples/batch-insert) -- batch inserts with shared `Ref` dependencies and per-row `SeqRef` overrides
Expand Down
19 changes: 10 additions & 9 deletions examples/basic/basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import (
"github.com/mhiro2/seedling/examples/basic"
)

func setup(t *testing.T) {
func setup(t *testing.T) *seedling.Registry {
t.Helper()
seedling.ResetRegistry()
basic.RegisterBlueprints()
reg := seedling.NewRegistry()
basic.RegisterBlueprints(reg)
return reg
}

func TestInsertOne_Company(t *testing.T) {
setup(t)
reg := setup(t)

company := seedling.InsertOne[basic.Company](t, nil)
company := seedling.NewSession[basic.Company](reg).InsertOne(t, nil)

if company.Root().ID == 0 {
t.Fatal("expected company ID to be set")
Expand All @@ -27,10 +28,10 @@ func TestInsertOne_Company(t *testing.T) {
}

func TestInsertOne_User(t *testing.T) {
setup(t)
reg := setup(t)

// InsertOne[User] automatically creates a parent Company.
user := seedling.InsertOne[basic.User](t, nil)
user := seedling.NewSession[basic.User](reg).InsertOne(t, nil)

if user.Root().ID == 0 {
t.Fatal("expected user ID to be set")
Expand All @@ -44,9 +45,9 @@ func TestInsertOne_User(t *testing.T) {
}

func TestInsertOne_UserWithSet(t *testing.T) {
setup(t)
reg := setup(t)

user := seedling.InsertOne[basic.User](t, nil,
user := seedling.NewSession[basic.User](reg).InsertOne(t, nil,
seedling.Set("Name", "alice"),
seedling.Set("Email", "alice@example.com"),
)
Expand Down
9 changes: 4 additions & 5 deletions examples/basic/blueprints.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ func nextID() int {
return int(idSeq.Add(1))
}

// RegisterBlueprints registers the Company and User blueprints.
// Call seedling.ResetRegistry() before this in tests to start fresh.
func RegisterBlueprints() {
seedling.MustRegister(seedling.Blueprint[Company]{
// RegisterBlueprints registers the Company and User blueprints in reg.
func RegisterBlueprints(reg *seedling.Registry) {
seedling.MustRegisterTo(reg, seedling.Blueprint[Company]{
Name: "company",
Table: "companies",
PKField: "ID",
Expand All @@ -29,7 +28,7 @@ func RegisterBlueprints() {
},
})

seedling.MustRegister(seedling.Blueprint[User]{
seedling.MustRegisterTo(reg, seedling.Blueprint[User]{
Name: "user",
Table: "users",
PKField: "ID",
Expand Down
15 changes: 8 additions & 7 deletions examples/batch-insert/batch_insert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,20 @@ import (
batchinsert "github.com/mhiro2/seedling/examples/batch-insert"
)

func setup(t *testing.T) {
func setup(t *testing.T) *seedling.Registry {
t.Helper()
seedling.ResetRegistry()
batchinsert.ResetIDs()
batchinsert.RegisterBlueprints()
reg := seedling.NewRegistry()
batchinsert.RegisterBlueprints(reg)
return reg
}

func TestInsertManyE_SharedProject(t *testing.T) {
// Arrange
setup(t)
reg := setup(t)

// Act
result, err := seedling.InsertManyE[batchinsert.Task](context.Background(), nil, 2,
result, err := seedling.NewSession[batchinsert.Task](reg).InsertManyE(context.Background(), nil, 2,
seedling.Ref("project", seedling.Set("Name", "shared-project")),
)
if err != nil {
Expand Down Expand Up @@ -60,10 +61,10 @@ func TestInsertManyE_SharedProject(t *testing.T) {

func TestInsertManyE_SeqRefCreatesDistinctProjects(t *testing.T) {
// Arrange
setup(t)
reg := setup(t)

// Act
result, err := seedling.InsertManyE[batchinsert.Task](context.Background(), nil, 2,
result, err := seedling.NewSession[batchinsert.Task](reg).InsertManyE(context.Background(), nil, 2,
seedling.SeqRef("project", func(i int) []seedling.Option {
return []seedling.Option{seedling.Set("Name", fmt.Sprintf("project-%d", i))}
}),
Expand Down
10 changes: 5 additions & 5 deletions examples/batch-insert/blueprints.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ func nextID() int {
return int(idSeq.Add(1))
}

// RegisterBlueprints registers Company, Project, and Task blueprints.
func RegisterBlueprints() {
seedling.MustRegister(seedling.Blueprint[Company]{
// RegisterBlueprints registers Company, Project, and Task blueprints in reg.
func RegisterBlueprints(reg *seedling.Registry) {
seedling.MustRegisterTo(reg, seedling.Blueprint[Company]{
Name: "company",
Table: "companies",
PKField: "ID",
Expand All @@ -28,7 +28,7 @@ func RegisterBlueprints() {
},
})

seedling.MustRegister(seedling.Blueprint[Project]{
seedling.MustRegisterTo(reg, seedling.Blueprint[Project]{
Name: "project",
Table: "projects",
PKField: "ID",
Expand All @@ -44,7 +44,7 @@ func RegisterBlueprints() {
},
})

seedling.MustRegister(seedling.Blueprint[Task]{
seedling.MustRegisterTo(reg, seedling.Blueprint[Task]{
Name: "task",
Table: "tasks",
PKField: "ID",
Expand Down
6 changes: 3 additions & 3 deletions examples/custom-defaults/blueprints.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ func nextID() int {
return int(idSeq.Add(1))
}

// RegisterBlueprints registers the User blueprint with sensible defaults.
func RegisterBlueprints() {
seedling.MustRegister(seedling.Blueprint[User]{
// RegisterBlueprints registers the User blueprint with sensible defaults in reg.
func RegisterBlueprints(reg *seedling.Registry) {
seedling.MustRegisterTo(reg, seedling.Blueprint[User]{
Name: "user",
Table: "users",
PKField: "ID",
Expand Down
Loading
Loading