Files
Decodal/doc/manual/source/embedding.md
T

6.0 KiB

Embedding

The Decodal runtime does not read filesystems, networks, or environment variables directly. The host injects global bindings and import resolution, then receives the evaluation result as Data or an application type.

Rust runtime

Engine owns an ImportLoader and global bindings. Use EmptyLoader for source that does not require external resources.

use decodal::{EmptyLoader, Engine, Value};

let mut engine = Engine::new(EmptyLoader);
engine.bind_global(
    "Service",
    Value::object([
        ("name", Value::string_type()),
        ("port", Value::int_type().gt(443).default_int(8443)?),
        ("enabled", Value::bool_type().default_bool(true)?),
    ]),
)?;

let module = engine.add_root_source(
    "service.dcdl",
    "service.dcdl",
    r#"{ name = "api"; port = 9443; } as Service"#,
)?;
let value = engine.eval_module(module)?;
let data = engine.materialize(&value)?;

Value is the public type for both concrete values and host-defined ranges. Its main constructors are:

  • Value::string, int, float, bool, array, and object: concrete values.
  • Value::string_type, int_type, float_type, and bool_type: primitive ranges.
  • Value::unknown: the top range, Unknown.
  • Value::array_of: an array range that applies one range to every element.
  • Value::map_of: an associative-array range that applies one range to every field value.
  • Value::object_with_rest: an object with named fields and a range for remaining fields.

Typed Rust integration

The Decodal derive from the derive feature generates DecodalSchema and DecodalDecode implementations from a Rust struct.

use decodal::{Decodal, DecodalDecode, DecodalSchema};

#[derive(Decodal)]
struct Service {
    name: String,

    #[decodal(gt = 443, default = 8443)]
    port: i64,

    #[decodal(rename = "feature.enable", default = true)]
    feature_enabled: bool,
}

DecodalSchema::decodal_schema() returns a Value suitable for Engine::bind_global. Materialized Data can be converted to a Rust type with DecodalDecode::decodal_decode.

For a struct that accepts additional fields, mark a map field with #[decodal(rest)].

use std::collections::BTreeMap;
use decodal::{Data, Decodal};

#[derive(Decodal)]
struct OpenConfig {
    enabled: bool,

    #[decodal(rest)]
    extra: BTreeMap<String, Data>,
}

BTreeMap<String, Data> corresponds to ...Unknown, while BTreeMap<String, String> corresponds to ...String. Named fields are excluded from the rest map. Only one #[decodal(rest)] field is allowed per struct, and it cannot be combined with rename, default, or field constraints.

Imports

ImportLoader::load returns either LoadedImport::Source or LoadedImport::Value. The host controls specifier resolution, reading from the filesystem or other storage, and content-type detection.

LoadedImport::Source supplies DCDL source. LoadedImport::Value is intended for structured host-defined representations of formats such as Markdown, JSON, and TOML.

import "./post.md"
  -> host reads and parses Markdown
  -> LoadedImport::Value {
       key: "content/post.md",
       value: { frontmatter: {...}, body: "..." }
     }
  -> normal Decodal validation and materialization

A stable key identifies an import and supplies diagnostic provenance. If a structured value violates a Decodal constraint, the diagnostic identifies the import key, logical value path, and source span of the violated DCDL constraint. The loader reports parse errors and positions in the external format itself.

Shared host environment

HostEnvironment combines loader construction and global-binding setup. Passing the same environment to the runtime, language service, and LSP keeps runtime and editor evaluation rules aligned.

use decodal::{Engine, HostEnvironment};

struct AppEnvironment;

impl HostEnvironment for AppEnvironment {
    type Loader = ContentLoader;

    fn create_loader(&self) -> Self::Loader {
        ContentLoader::new()
    }

    fn configure_engine(
        &self,
        engine: &mut Engine<Self::Loader>,
    ) -> decodal::Result<()> {
        engine.bind_global("Site", site_schema())?;
        Ok(())
    }
}

Construct transport-independent tooling with decodal_language_service::LanguageService::new(&environment). For LSP integration, implement decodal_lsp::LspEnvironment and start the server from an environment factory.

use decodal_lsp::{LspEnvironment, run_stdio};

impl LspEnvironment for AppEnvironment {}

run_stdio(|initialize| {
    let _ = initialize;
    Ok(AppEnvironment)
})?;

The factory receives the client's InitializeParams once, before the server responds with its capabilities. Use the LspEnvironment document lifecycle hooks to reflect unsaved DCDL, Markdown, and other resources in a host-owned overlay.

JavaScript and browser environments

The DecodalLanguageService in decodal-wasm does not assume a filesystem or virtual project. JavaScript supplies globals, loadImport, and completeImport.

import init, { DecodalLanguageService } from 'decodal-wasm';

await init();

const files = {
  'schema.dcdl': 'Server = { port = Int; };',
};

const service = new DecodalLanguageService({
  globals: {
    App: {
      enabled: { $decodal: 'Bool', default: true },
    },
  },
  loadImport(_currentKey, specifier) {
    const key = specifier.replace(/^\.\//, '');
    return { kind: 'source', key, name: key, source: files[key] };
  },
  completeImport() {
    return ['./schema.dcdl'];
  },
});

const result = service.evaluate('main.dcdl', 'main.dcdl', 'App');

JavaScript primitives, arrays, and plain objects become concrete values. Use $decodal descriptors for ranges:

  • { $decodal: 'String' | 'Int' | 'Float' | 'Bool' }
  • { $decodal: 'Unknown' }
  • { $decodal: 'Array', item }
  • { $decodal: 'Map', value }
  • { $decodal: 'Object', fields, rest }
  • { $decodal: 'Range', constraints, default }

Import callbacks are synchronous. Preload or cache asynchronous inputs such as network resources before evaluation.