# Package settings

An installable app declares its configuration as one typed Go struct. A deployment
can override tagged fields with `CICADEL_<APP>_<NAME>` environment variables.

## How it works

- The project passes the values in Go.
- A deployment may override tagged fields under the app's own prefix.
- The application resolves and validates the settings before `Start`.
- `manage.go check` reports an invalid key without printing its value.
- Secret fields are never displayed.

> [!NOTE]
> Unlike Django, there is no global `settings` object for apps. Each app reads its
> own resolved struct directly.

## Declare settings

1. Tag each field a deployment may change with `setting:"NAME"`.
2. Add `,secret` for credentials.
3. Leave handles and callbacks untagged. They are set only in Go.

Supported types are `string`, `bool`, integers, unsigned integers, floats,
`time.Duration` and `[]string` (comma separated).

<!-- example: installable/apps/newsletter/app.go -->
```go
package newsletter

import (
    "context"
    "errors"
    "net/http"
    "strings"
    "time"

    "github.com/fauzaanu/cicadel"
    "github.com/fauzaanu/cicadel/response"
    "github.com/fauzaanu/cicadel/settings"
    "github.com/fauzaanu/cicadel/urls"
)

// Options is configured by the project. A deployment may override tagged
// fields with CICADEL_NEWSLETTER_FROM, CICADEL_NEWSLETTER_BATCH,
// CICADEL_NEWSLETTER_INTERVAL and CICADEL_NEWSLETTER_API_KEY.
type Options struct {
    From     string        `setting:"FROM"`
    Batch    int           `setting:"BATCH"`
    Interval time.Duration `setting:"INTERVAL"`
    APIKey   string        `setting:"API_KEY,secret"`
}

// Validate runs after environment overrides. Name the field, never the value.
func (o Options) Validate() error {
    if !strings.Contains(o.From, "@") {
        return errors.New("From must be an email address")
    }
    if o.Batch < 1 || o.Batch > 1000 {
        return errors.New("Batch must be 1–1000")
    }
    return nil
}

func New(options Options) cicadel.App {
    if options.Batch == 0 {
        options.Batch = 100
    }
    config := settings.NewPackage(options)
    return cicadel.App{
        Name:     "newsletter",
        Settings: config,
        Start: func(context.Context) (cicadel.Cleanup, error) {
            _ = config.Value().Interval // resolved and valid here
            return nil, nil
        },
        Routes: []urls.Route{
            urls.Path("GET", "/newsletter/", "newsletter:index",
                func(w http.ResponseWriter, r *http.Request) {
                    if err := response.JSON(w, http.StatusOK, map[string]int{"batch": config.Value().Batch}); err != nil {
                        response.Error(w, http.StatusInternalServerError)
                    }
                }),
        },
    }
}
```

`settings.NewPackage` records the project's values without reading the
environment. Construct a fresh one inside the app constructor, as above, so each
application gets its own.

If `Options` has a `Validate() error` method, it runs after overrides.

## Configure them in the project

Install the app in `project/apps.go` with explicit Go values:

```go
newsletter.New(newsletter.Options{From: "news@example.com", Interval: time.Hour})
```

`cicadel.New` resolves every app's settings in dependency order. It reads the
environment that `settings.Load` read (`Settings.Env`).

Override one value per deployment:

```sh
CICADEL_NEWSLETTER_BATCH=500 go run manage.go runserver
```

### In tests

Settings from `settings.Defaults()` or `apptest.Settings()` read no environment.
Tests see only the Go values. Inject overrides with
`settings.LoadWith(apptest.Settings(), lookup)`.

## Inspect and check

`go run manage.go apps` lists each key, its current value and where it came from.
Secret values print as `[redacted]`:

```text
newsletter (local)
  CICADEL_NEWSLETTER_FROM="news@example.com" (default)
  CICADEL_NEWSLETTER_BATCH="500" (environment)
  CICADEL_NEWSLETTER_INTERVAL="1h0m0s" (default)
  CICADEL_NEWSLETTER_API_KEY="[redacted]" (default)
```

An invalid value does not stop construction. Instead:

- `go run manage.go check` (and `check --json`) reports it as
  `<app>:settings:invalid`. It names the key but not the value.
- `Start` refuses to run.

```text
error newsletter:settings:invalid: CICADEL_NEWSLETTER_BATCH must be an integer in range
```

Validation errors read `app newsletter settings: Batch must be 1–1000`.
`Application.PackageSettings` returns the same redacted description for tools.

## Rules

- Keys are always `CICADEL_<APP>_<NAME>`, with `NAME` in upper snake case.
- `cicadel.New` rejects:
  - a key that collides with a project setting (`settings.CoreKeys`);
  - a key that collides with another app's key;
  - a duplicate name;
  - an unsupported field type.
- A system check named `settings` is reserved on apps that declare settings.
- Settings resolve once. Sharing one `settings.Package` between two apps is an error.
- A package that needs its values during construction resolves them itself with
  `Package.Resolve(app, s.Env())`. It still sets `App.Settings` to the same value,
  so `apps` and `check` see it.
- `settings.LoadPackage` resolves without keeping the result.

## Accounts settings

The standard accounts app uses this contract. Generated projects set
`AccountOptions` in `project/accounts.go`. A deployment may override:

- `CICADEL_ACCOUNTS_SESSION_LIFETIME`
- `CICADEL_ACCOUNTS_RECOVERY_LIFETIME`
- `CICADEL_ACCOUNTS_IDLE_TIMEOUT`
- `CICADEL_ACCOUNTS_BCRYPT_COST` (10–16)

With an invalid value, the project builds without account services. `check`
reports the key and `runserver` refuses to start.
