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
4 changes: 3 additions & 1 deletion docs/plans/2026-10-04-plan-builder/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The module path is `example.com/app`.

## What each file shows

Each directory has a `README.md` that walks through its code.

| File | What it shows |
|---|---|
| [`main.go`](main.go) | A client, one cipher for each tenant, and a batch of two types in one request |
Expand All @@ -18,7 +20,7 @@ The module path is `example.com/app`.
| [`accounts/account.go`](accounts/account.go) | An embedded `gorm.Model`, another library's tags, an unexported field and `-redact` |
| [`contacts/contacts.go`](contacts/contacts.go) | [`crm.Contact`](crm/contact.go), a type in another package, in separate columns, with a model |
| [`documents/documents.go`](documents/documents.go) | An `opaque` struct, sealed as one value |
| [`proto/`](proto/) | A protobuf message whose fields carry data categories, which generates [`internal/pb/`](internal/pb/) |
| [`proto/`](proto/) | A protobuf message whose fields have data categories, which generates [`internal/pb/`](internal/pb/) |
| [`rules/rules.go`](rules/rules.go) | Rules that decide what to encrypt from each field's data categories |
| [`cmd/genencrypt/main.go`](cmd/genencrypt/main.go) | The generate program that runs the rules |
| [`individuals/`](individuals/) | The file the rules give for the protobuf message, and a store that uses it |
Expand Down
143 changes: 143 additions & 0 deletions docs/plans/2026-10-04-plan-builder/accounts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Preserve GORM fields and redact an account

This example shows an embedded GORM model, copied tags, and generated print methods.
It also distinguishes an unexported field from an omitted field.
The Golang SDK for CipherStash Stack is planned and does not exist yet, so this code does not build in this repository.
The [Go example index](../README.md) lists every example and what was checked.

`account_stash.go` is written by hand.
It shows what `stashgen` is planned to write.

## What you will learn

- Tag an embedded GORM model and distinguish two fields that are not stored.
- Inspect the encrypted type, copied tags, warnings, and declaration.
- Add redacted print methods to the tagged struct.
- Give the encrypted type to GORM.

## Tag the account fields

- **Purpose:** Understand how one tag covers an embedded struct from another package.
- **Related:** [The plan's embedded struct design](../../2026-10-04-plan-builder.md#embedded-structs-unexported-fields-and-other-tags), [the plan's printing design](../../2026-10-04-plan-builder.md#printing)

The `stash:",passthrough"` tag stores every `gorm.Model` field as a passthrough field.
`Email` becomes one Encrypt Query Language (EQL) column with the `TextEq` type.
`stashgen` copies the `gorm` and `json` tags of `Email` to the same field of `EncryptedAccount`.

[`account.go`, lines 12 to 21](account.go#L12-L21)

```go
//go:generate go tool stashgen -type Account -redact

type Account struct {
_ struct{} `stash:"context=accounts"`

// One tag decides for every field of gorm.Model, which cannot carry tags.
gorm.Model `stash:",passthrough"`

// stashgen copies the gorm and json tags onto EncryptedAccount.
Email string `stash:"email,encrypt_into=TextEq" gorm:"uniqueIndex" json:"email"`
```

The unexported `cache` field has no tag, so the generator ignores it with three notices:

- `stashgen` prints a warning.
- The generated file names `cache` in a comment.
- The program prints a warning to stderr once for each type, on first use.

The `token` field uses `stash:"-"`, so it is an omitted field and produces no warning.

[`account.go`, lines 23 to 29](account.go#L23-L29)

```go
// An unexported field with no stash tag is ignored, and stashgen and the
// running program both warn about it.
cache string

// The tag acknowledges the skip, so nothing warns.
token string `stash:"-"`
}
```

## Inspect the encrypted account

- **Purpose:** Follow copied tags and fields that are not stored into the generated file.
- **Related:** [The plan's generated file design](../../2026-10-04-plan-builder.md#what-stashgen-writes), [the plan's embedded struct design](../../2026-10-04-plan-builder.md#embedded-structs-unexported-fields-and-other-tags)

The generated comment names `cache`, and `EncryptedAccount` embeds the same `gorm.Model`.
The encrypted `Email` field retains its GORM and JSON tags.

[`account_stash.go`, lines 19 to 25](account_stash.go#L19-L25)

```go
// Not encrypted and not stored: the unexported field "cache". Tag it
// `stash:"-"` to confirm that.

type EncryptedAccount struct {
gorm.Model
Email eql.TextEq `gorm:"uniqueIndex" json:"email"`
}
```

The codec's `Unexported` list enables the planned runtime warning for `cache`.
The declaration omits `token` and does not include `cache`.

[`account_stash.go`, lines 55 to 66](account_stash.go#L55-L66)

```go
var declaration = gensupport.Declare("accounts").
Passthrough("id").
Passthrough("created_at").
Passthrough("updated_at").
Passthrough("deleted_at").
EncryptInto("email", "TextEq").
Omit("token")

var codec = gensupport.New(gensupport.Generated[Account, EncryptedAccount]{
TypeName: "Account",
Declaration: declaration,
Unexported: []string{"cache"},
```

## Redact the tagged struct

- **Purpose:** See what the `-redact` flag adds to `Account`.
- **Related:** [The plan's printing design](../../2026-10-04-plan-builder.md#printing)

`-redact` asks `stashgen` to add `String` and `LogValue` methods to `Account`.
These methods show `gorm.Model` and hide `Email`.

[`account_stash.go`, lines 35 to 42](account_stash.go#L35-L42)

```go
// Written because of -redact: Account no longer prints its sealed fields.
func (a Account) String() string {
return gensupport.Redacted("Account", map[string]any{"Model": a.Model}, "Email")
}

func (a Account) LogValue() slog.Value {
return gensupport.RedactedLog(map[string]any{"Model": a.Model}, "Email")
}
```

## Give encrypted accounts to GORM

- **Purpose:** Follow a slice through encryption and into GORM.
- **Related:** [The plan's database design](../../2026-10-04-plan-builder.md#databases)

`EncryptedAccount.TableName` maps the encrypted type to the `accounts` table.
`Create` encrypts the whole slice before GORM receives it.

[`account.go`, lines 31 to 39](account.go#L31-L39)

```go
func (EncryptedAccount) TableName() string { return "accounts" }

func Create(ctx context.Context, db *gorm.DB, cipher *encrypt.Cipher, accounts []Account) error {
encrypted, err := Encrypt(ctx, cipher, accounts)
if err != nil {
return err
}
return db.WithContext(ctx).Create(&encrypted).Error
}
```
46 changes: 46 additions & 0 deletions docs/plans/2026-10-04-plan-builder/cmd/genencrypt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Generate encryption code from protobuf rules

This example contains the command that is planned to turn protobuf rules into a generated file.
It connects the protobuf source, `rules.Individuals`, and the requested output path.
The Golang SDK for CipherStash Stack is planned and does not exist yet, so this code does not build in this repository.
The [Go example index](../../README.md) lists every example and what was checked.

## What you will learn

- Run `stashgen.Generate` with a protobuf source and policy.
- Review rule changes in the generated file.

## Connect the source, rules, and output

- **Purpose:** Follow the three inputs to the planned library generator.
- **Related:** [The plan's policy declarations](../../../2026-10-04-plan-builder.md#declarations-from-a-policy), [the rules walkthrough](../../rules/README.md), [the individuals walkthrough](../../individuals/README.md)

`protosource.New()` supplies protobuf descriptors and field options.
`rules.Individuals` supplies the field decisions.
`stashgen.Output` selects the generated file in the `individuals` package.
The output goes in your own `individuals` package, not `internal/pb`, which contains the generated type.
This separation stops the generate program from importing a file that it writes.

[`main.go`, lines 12 to 18](main.go#L12-L18)

```go
func main() {
err := stashgen.Generate(protosource.New(), rules.Individuals,
stashgen.Output("../individuals/individual_stash.go"))
if err != nil {
log.Fatal(err)
}
}
```

The `stashgen`, `protosource`, and policy packages do not exist yet.

## Review rule changes in the generated file

- **Purpose:** See how reviewers inspect policy changes without running rules in the application.
- **Related:** [The generated individuals walkthrough](../../individuals/README.md), [the rules walkthrough](../../rules/README.md)

The [`rules` package](../../rules/README.md) runs this command through `go generate`.
The application calls the resulting `individuals.Encrypt` and `individuals.Decrypt` functions.
A rule change changes `individual_stash.go`, which gives a reviewer a concrete generated diff.
The application never runs the rules.
Loading
Loading