Decodal/doc/manual/souce/design/embedding-api.md

139 lines
3.7 KiB
Markdown

# Embedding API
Decodal core can be embedded without giving the core crate access to a filesystem.
The host supplies imported sources through `SourceLoader` and may also provide global bindings through the host prelude API.
## Host prelude
`Engine` owns a prelude environment.
Bindings in this environment are visible from every module loaded by the engine.
```text
prelude env
module root env
let / function env
```
Module top-level bindings shadow prelude bindings.
Primitive type names such as `String`, `Int`, `Float`, `Bool`, and `Array` are handled before environment lookup, so they are reserved and cannot be shadowed by host bindings.
## Global bindings
The host can bind values before adding or evaluating user sources.
```rust
use decodal::{EmptyLoader, Engine, HostValue};
let mut engine = Engine::new(EmptyLoader);
engine.bind_global(
"Service",
HostValue::object([
("name", HostValue::string_type()),
("port", HostValue::int_type().gt(443).default_int(8443)?),
("enabled", HostValue::bool_type().default_bool(true)?),
]),
)?;
```
A user source can then refer to `Service` without importing it.
```dcdl
Service & {
name = "api";
port = 9443;
}
```
## HostValue
`HostValue` is the public builder-facing value representation for embedding.
It keeps host code from constructing internal `ThunkId` or `ObjectValue` values directly.
```text
HostValue =
String
Int
Float
Bool
Array(Vec<HostValue>)
Object(Vec<HostField>)
Abstract { constraints, default }
```
When a host value is bound, the engine internalizes it into `RuntimeValue` and allocates value thunks for object fields, array items, and defaults.
## Abstract host objects
A host-provided schema object is represented as a concrete object structure whose fields may contain abstract values.
```rust
HostValue::object([
("name", HostValue::string_type()),
("port", HostValue::int_type().gt(443).default_int(8443)?),
])
```
Conceptually this becomes:
```text
Concrete(Object {
name -> Thunk(Abstract { constraints: [String], default: none })
port -> Thunk(Abstract { constraints: [Int, > 443], default: 8443 })
})
```
This matches the runtime model used for Decodal source-defined schema objects.
## Typed Rust integration
Hosts can use `decodal-derive` to keep a Rust struct, the Decodal schema, and the decoded result in sync.
The derive implements two traits from the `decodal` crate:
- `DecodalSchema`: builds a `HostValue` schema that can be passed to `Engine::bind_global`.
- `DecodalDecode`: decodes materialized `Data` back into the Rust type.
```rust
use decodal::{DecodalDecode, DecodalSchema, EmptyLoader, Engine};
use decodal_derive::Decodal;
#[derive(Decodal)]
struct Service {
name: String,
#[decodal(gt = 443, default = 8443)]
port: i64,
#[decodal(rename = "feature.enable", default = true)]
feature_enabled: bool,
}
let mut engine = Engine::new(EmptyLoader);
engine.bind_global("Service", Service::decodal_schema())?;
let value = engine.eval_module(module)?;
let data = engine.materialize(&value)?;
let service = Service::decodal_decode(&data)?;
```
Supported field attributes are intentionally small:
- `rename = "path.to.field"`
- `default`
- `default = value`
- numeric constraints: `gt`, `gte`, `lt`, `lte`
The derive does not add host callbacks or reflection.
It only generates schema construction and typed decoding code.
## SourceLoader and prelude together
`SourceLoader` and host prelude bindings are independent mechanisms.
- Use `SourceLoader` when user sources should explicitly import host-provided modules.
- Use prelude bindings when host-provided schemas or constants should be globally available.
Both mechanisms share the same runtime evaluator, thunk model, and materialization rules.