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
39 changes: 39 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Migration Guide

- [Unreleased: runtime type bounds](#unreleased-runtime-type-bounds)
- [Unreleased: HTTP response contracts](#unreleased-http-response-contracts)
- [Unreleased: application and route composition](#unreleased-application-and-route-composition)
- [Unreleased: documented method routers](#unreleased-documented-method-routers)
Expand All @@ -12,6 +13,44 @@

---

# Unreleased: runtime type bounds

Initialized values no longer have to implement loading or default-construction traits just to
be stored in a context, extracted by handlers, or used by messages and scheduled tasks.

- Use `Gotcha::from_state(value)` for state without `Default` and automatic empty configuration.
- Use `Gotcha::from_context(GotchaContext { state, config })` when both values are already ready.
State and application configuration only need `Clone + Send + Sync + 'static` for runtime use.
- `with_state::<S>()`, `with_config::<C>()`, and `with_types::<S, C>()` retain their default-based
convenience behavior. Only these constructors choose automatic initialization and require its
bounds; creating defaults and reading files still happen at startup, after any explicit overrides.
- `.state(...)`, `.config(...)`, route composition, task registration, and serving no longer carry
unrelated `Default` or serde requirements. `.build_config(...)` requires `C: DeserializeOwned`.

`ConfigWrapper<T>` and `GotchaContext<S, C>` impose no bounds at their type definitions.
`ConfigWrapper`'s derived implementations request `Serialize`, `Deserialize`, `Clone`, or `Default`
only for the corresponding operation. `#[state]`, `#[config]`, and `FromRef` extraction no longer
impose loading bounds on the stored configuration or unrelated state type.

For the trait API, `GotchaApp::Config` only requires the runtime bounds. `build_router(context)`
can therefore accept an explicit configuration without serde or `Default`. `config()` and `run()`
require `Self::Config: DeserializeOwned`, because their contract includes configuration loading.
This method bound applies even when `config()` is overridden; use an explicit context if the
configuration cannot be deserialized. Generic callers of these methods must now state that bound
explicitly. `Serialize` and `Default` are no longer required by trait-based loading.

The existing `GotchaConfig` marker retains its original bounds for compatibility with user generic
code. Runtime containers and the extraction macros no longer require it.

Explicit configuration still wins over registered sources. Selected sources remain strict, and
automatic default loading retains its warning and fallback behavior. Listener address precedence
is unchanged: builder `run()` uses `.host()`/`.port()`, while `listen()`/`listen_on()` use their
argument; providing `ConfigWrapper::server` does not change that builder policy.

See the [initialized-values example](README.md#initialized-state-and-configuration).

---

# Unreleased: HTTP response contracts

**`Schematic` no longer implies `Responsible`.** A data schema does not determine how a value is
Expand Down
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,52 @@ struct User {
}
```

### Initialized state and configuration

Use `Gotcha::from_state(value)` for state such as an initialized connection pool. To supply both
state and configuration, use `Gotcha::from_context(context)`. These values only need
`Clone + Send + Sync + 'static`; neither needs `Default`, and configuration needs no serde traits:

```rust,no_run
use gotcha::prelude::*;

#[state]
#[derive(Clone)]
struct AppState { started_at: std::time::Instant }

#[config]
#[derive(Clone)]
struct AppConfig { deployment: String }

let app = Gotcha::from_context(GotchaContext {
state: AppState { started_at: std::time::Instant::now() },
config: ConfigWrapper {
server: ServerConfig::default(),
app: AppConfig { deployment: "production".into() },
},
})
.get("/deployment", |State(config): State<AppConfig>| async move { config.deployment });
```

`with_state::<T>()` and `with_types::<S, C>()` remain available when you want automatic default
construction. File loading requires `Deserialize`; serializing a configuration requires `Serialize`.

### Advanced Trait API (For complex applications)

```rust,no_run
use gotcha::prelude::*;

#[config]
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
#[derive(Debug, Deserialize, Clone)]
pub struct Config {
pub database_url: String,
pub redis_url: String,
}

#[state]
#[derive(Clone, Default)]
#[derive(Clone)]
pub struct AppState {
pub started_at: u64,
pub started_at: std::time::Instant,
}

pub struct App {}
Expand All @@ -82,7 +112,7 @@ impl GotchaApp for App {
async fn state(&self, config: &ConfigWrapper<Self::Config>) -> GotchaResult<Self::State> {
// Open database connections here; `config` is already loaded.
let _ = &config.database_url;
Ok(AppState::default())
Ok(AppState { started_at: std::time::Instant::now() })
}
}

Expand Down
Loading
Loading