From b8404df0476815be735e47f28cf5184616f0e275 Mon Sep 17 00:00:00 2001 From: Hare Date: Fri, 26 Jun 2026 00:39:33 +0900 Subject: [PATCH] Add typed Decodal derive support --- Cargo.lock | 16 +- Cargo.toml | 3 +- README.md | 29 +++ crates/decodal-cli/Cargo.toml | 2 +- crates/decodal-core/src/constraints.rs | 5 +- crates/decodal-core/src/embedding.rs | 51 ++++ crates/decodal-core/src/eval.rs | 4 + crates/decodal-core/src/lib.rs | 5 + crates/decodal-core/src/runtime.rs | 1 + crates/decodal-core/src/typed.rs | 227 ++++++++++++++++++ crates/decodal-derive/Cargo.toml | 22 ++ crates/decodal-derive/src/lib.rs | 212 ++++++++++++++++ crates/decodal-derive/tests/derive.rs | 79 ++++++ crates/decodal-wasm/Cargo.toml | 2 +- doc/manual/souce/design/embedding-api.md | 43 +++- doc/manual/souce/development.md | 3 + doc/manual/souce/language/value/index.md | 3 +- package.nix | 2 +- site/decodal-site/src/wasm/README.md | 29 +++ .../src/wasm/decodal_wasm_bg.wasm | Bin 300219 -> 299962 bytes site/decodal-site/src/wasm/package.json | 2 +- 21 files changed, 729 insertions(+), 11 deletions(-) create mode 100644 crates/decodal-core/src/typed.rs create mode 100644 crates/decodal-derive/Cargo.toml create mode 100644 crates/decodal-derive/src/lib.rs create mode 100644 crates/decodal-derive/tests/derive.rs diff --git a/Cargo.lock b/Cargo.lock index df3dc12..525429a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,21 +25,31 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "decodal" -version = "0.1.0" +version = "0.1.1" dependencies = [ "regex", ] [[package]] name = "decodal-cli" -version = "0.1.0" +version = "0.1.1" dependencies = [ "decodal", ] +[[package]] +name = "decodal-derive" +version = "0.1.1" +dependencies = [ + "decodal", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "decodal-wasm" -version = "0.1.0" +version = "0.1.1" dependencies = [ "decodal", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index c4c78af..4c451ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,12 @@ members = [ "crates/decodal-core", "crates/decodal-cli", "crates/decodal-wasm", + "crates/decodal-derive", ] resolver = "2" [workspace.package] -version = "0.1.0" +version = "0.1.1" edition = "2024" rust-version = "1.85" license = "MIT OR Apache-2.0" diff --git a/README.md b/README.md index 8db9ac1..d2b7429 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,35 @@ Embedded hosts should depend on `decodal` and provide imports with a `SourceLoad decodal = "0.1" ``` +## Derive support + +For embedded Rust applications, `decodal-derive` can generate a Decodal schema and typed decoder from a Rust struct. + +```toml +[dependencies] +decodal = "0.1" +decodal-derive = "0.1" +``` + +```rust +use decodal::{DecodalDecode, DecodalSchema, 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, +} +``` + +The derive implements: + +- `DecodalSchema`, which produces a host schema for `Engine::bind_global` +- `DecodalDecode`, which converts materialized `Data` into the Rust struct + ## CLI A standalone CLI is kept in this repository as the `decodal-cli` workspace package. diff --git a/crates/decodal-cli/Cargo.toml b/crates/decodal-cli/Cargo.toml index acbc3a2..d71f434 100644 --- a/crates/decodal-cli/Cargo.toml +++ b/crates/decodal-cli/Cargo.toml @@ -20,4 +20,4 @@ default = [] regex = ["decodal/regex"] [dependencies] -decodal = { version = "0.1.0", path = "../decodal-core" } +decodal = { version = "0.1.1", path = "../decodal-core" } diff --git a/crates/decodal-core/src/constraints.rs b/crates/decodal-core/src/constraints.rs index 3ba6098..bfc7a1f 100644 --- a/crates/decodal-core/src/constraints.rs +++ b/crates/decodal-core/src/constraints.rs @@ -70,7 +70,10 @@ pub fn normalize_constraints( if matches!( primitive, - Some((PrimitiveType::String | PrimitiveType::Bool, _)) + Some(( + PrimitiveType::String | PrimitiveType::Bool | PrimitiveType::Array, + _ + )) ) && (lower.is_some() || upper.is_some()) { let mut diagnostic = Diagnostic::new( diff --git a/crates/decodal-core/src/embedding.rs b/crates/decodal-core/src/embedding.rs index 57654b3..e544441 100644 --- a/crates/decodal-core/src/embedding.rs +++ b/crates/decodal-core/src/embedding.rs @@ -63,6 +63,18 @@ impl HostValue { ) } + pub fn object_from_paths(fields: I) -> Self + where + I: IntoIterator, + N: Into, + { + let mut root = Vec::new(); + for (path, value) in fields { + insert_path(&mut root, &path.into(), value); + } + Self::Object(root) + } + pub fn string_type() -> Self { Self::abstract_with_constraint(Constraint::Type(PrimitiveType::String)) } @@ -79,6 +91,10 @@ impl HostValue { Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Bool)) } + pub fn array_type() -> Self { + Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Array)) + } + pub fn builtin_predicate(name: impl Into) -> Self { Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into())) } @@ -162,3 +178,38 @@ impl HostValue { self.default(Self::bool(value)) } } + +impl HostField { + pub fn new(name: impl Into, value: HostValue) -> Self { + Self { + name: name.into(), + value, + } + } +} + +fn insert_path(fields: &mut Vec, path: &str, value: HostValue) { + let mut parts = path.splitn(2, '.'); + let Some(head) = parts.next().filter(|part| !part.is_empty()) else { + return; + }; + if let Some(tail) = parts.next() { + if let Some(field) = fields.iter_mut().find(|field| field.name == head) { + if let HostValue::Object(children) = &mut field.value { + insert_path(children, tail, value); + } else { + let mut children = Vec::new(); + insert_path(&mut children, tail, value); + field.value = HostValue::Object(children); + } + } else { + let mut children = Vec::new(); + insert_path(&mut children, tail, value); + fields.push(HostField::new(head, HostValue::Object(children))); + } + } else if let Some(field) = fields.iter_mut().find(|field| field.name == head) { + field.value = value; + } else { + fields.push(HostField::new(head, value)); + } +} diff --git a/crates/decodal-core/src/eval.rs b/crates/decodal-core/src/eval.rs index dc52280..58e2271 100644 --- a/crates/decodal-core/src/eval.rs +++ b/crates/decodal-core/src/eval.rs @@ -1170,6 +1170,7 @@ fn primitive_type(name: &str) -> Option { "Int" => Some(PrimitiveType::Int), "Float" => Some(PrimitiveType::Float), "Bool" => Some(PrimitiveType::Bool), + "Array" => Some(PrimitiveType::Array), _ => None, } } @@ -1250,6 +1251,9 @@ fn value_matches_primitive(value: &RuntimeValue, primitive: PrimitiveType) -> bo ) | ( RuntimeValue::Concrete(ConcreteValue::Bool(_)), PrimitiveType::Bool + ) | ( + RuntimeValue::Concrete(ConcreteValue::Array(_)), + PrimitiveType::Array ) ) } diff --git a/crates/decodal-core/src/lib.rs b/crates/decodal-core/src/lib.rs index b81c058..f05f5a7 100644 --- a/crates/decodal-core/src/lib.rs +++ b/crates/decodal-core/src/lib.rs @@ -12,6 +12,7 @@ pub mod module; pub mod parser; pub mod runtime; pub mod span; +pub mod typed; pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param}; pub use constraints::normalize_constraints; @@ -23,6 +24,10 @@ pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader}; pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id}; pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue}; pub use span::{SourceId, Span}; +pub use typed::{ + DecodalDecode, DecodalSchema, DecodeError, DecodeResult, IntoHostValue, data_at_path, + decode_path, prefix_decode_error, +}; pub fn version() -> &'static str { env!("CARGO_PKG_VERSION") diff --git a/crates/decodal-core/src/runtime.rs b/crates/decodal-core/src/runtime.rs index 2366e8f..49d4311 100644 --- a/crates/decodal-core/src/runtime.rs +++ b/crates/decodal-core/src/runtime.rs @@ -79,6 +79,7 @@ pub enum PrimitiveType { Int, Float, Bool, + Array, } #[derive(Debug, Clone, PartialEq)] diff --git a/crates/decodal-core/src/typed.rs b/crates/decodal-core/src/typed.rs new file mode 100644 index 0000000..4c47a31 --- /dev/null +++ b/crates/decodal-core/src/typed.rs @@ -0,0 +1,227 @@ +use alloc::{format, string::String, vec::Vec}; + +use crate::{Data, HostValue}; + +pub trait DecodalSchema { + fn decodal_schema() -> HostValue; +} + +pub trait DecodalDecode: Sized { + fn decodal_decode(data: &Data) -> DecodeResult; +} + +pub trait IntoHostValue { + fn into_host_value(self) -> HostValue; +} + +pub type DecodeResult = core::result::Result; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DecodeError { + pub path: String, + pub message: String, +} + +impl DecodeError { + pub fn new(path: impl Into, message: impl Into) -> Self { + Self { + path: path.into(), + message: message.into(), + } + } + + pub fn at_type(path: &str, expected: &'static str) -> Self { + Self::new(path, format!("expected {expected}")) + } +} + +impl core::fmt::Display for DecodeError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + if self.path.is_empty() { + write!(f, "{}", self.message) + } else { + write!(f, "{}: {}", self.path, self.message) + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for DecodeError {} + +pub fn decode_path(data: &Data, path: &str) -> DecodeResult { + let value = data_at_path(data, path).ok_or_else(|| DecodeError::new(path, "missing field"))?; + T::decodal_decode(value).map_err(|error| prefix_error(path, error)) +} + +pub fn data_at_path<'a>(data: &'a Data, path: &str) -> Option<&'a Data> { + let mut current = data; + for part in path.split('.') { + if part.is_empty() { + return None; + } + let Data::Object(fields) = current else { + return None; + }; + current = &fields.iter().find(|field| field.name == part)?.value; + } + Some(current) +} + +pub fn prefix_decode_error(path: &str, error: DecodeError) -> DecodeError { + prefix_error(path, error) +} + +fn prefix_error(path: &str, mut error: DecodeError) -> DecodeError { + if error.path.is_empty() { + error.path = String::from(path); + } else if !path.is_empty() { + error.path = format!("{path}.{}", error.path); + } + error +} + +impl DecodalSchema for String { + fn decodal_schema() -> HostValue { + HostValue::string_type() + } +} + +impl DecodalDecode for String { + fn decodal_decode(data: &Data) -> DecodeResult { + match data { + Data::String(value) => Ok(value.clone()), + _ => Err(DecodeError::at_type("", "String")), + } + } +} + +impl IntoHostValue for String { + fn into_host_value(self) -> HostValue { + HostValue::string(self) + } +} + +impl IntoHostValue for &str { + fn into_host_value(self) -> HostValue { + HostValue::string(self) + } +} + +macro_rules! impl_int_decode { + ($($ty:ty),* $(,)?) => { + $( + impl DecodalSchema for $ty { + fn decodal_schema() -> HostValue { + HostValue::int_type() + } + } + + impl DecodalDecode for $ty { + fn decodal_decode(data: &Data) -> DecodeResult { + match data { + Data::Int(value) => <$ty>::try_from(*value) + .map_err(|_| DecodeError::new("", "integer value is out of range")), + _ => Err(DecodeError::at_type("", "Int")), + } + } + } + + impl IntoHostValue for $ty { + fn into_host_value(self) -> HostValue { + HostValue::int(self as i64) + } + } + )* + }; +} + +impl_int_decode!(i8, i16, i32, i64, u8, u16, u32); + +impl DecodalSchema for f64 { + fn decodal_schema() -> HostValue { + HostValue::float_type() + } +} + +impl DecodalDecode for f64 { + fn decodal_decode(data: &Data) -> DecodeResult { + match data { + Data::Float(value) => Ok(*value), + Data::Int(value) => Ok(*value as f64), + _ => Err(DecodeError::at_type("", "Float")), + } + } +} + +impl IntoHostValue for f64 { + fn into_host_value(self) -> HostValue { + HostValue::float(self) + } +} + +impl DecodalSchema for f32 { + fn decodal_schema() -> HostValue { + HostValue::float_type() + } +} + +impl DecodalDecode for f32 { + fn decodal_decode(data: &Data) -> DecodeResult { + f64::decodal_decode(data).map(|value| value as f32) + } +} + +impl IntoHostValue for f32 { + fn into_host_value(self) -> HostValue { + HostValue::float(f64::from(self)) + } +} + +impl DecodalSchema for bool { + fn decodal_schema() -> HostValue { + HostValue::bool_type() + } +} + +impl DecodalDecode for bool { + fn decodal_decode(data: &Data) -> DecodeResult { + match data { + Data::Bool(value) => Ok(*value), + _ => Err(DecodeError::at_type("", "Bool")), + } + } +} + +impl IntoHostValue for bool { + fn into_host_value(self) -> HostValue { + HostValue::bool(self) + } +} + +impl DecodalSchema for Vec { + fn decodal_schema() -> HostValue { + HostValue::array_type() + } +} + +impl DecodalDecode for Vec { + fn decodal_decode(data: &Data) -> DecodeResult { + match data { + Data::Array(items) => items + .iter() + .enumerate() + .map(|(index, item)| { + T::decodal_decode(item) + .map_err(|error| prefix_error(&format!("[{index}]"), error)) + }) + .collect(), + _ => Err(DecodeError::at_type("", "Array")), + } + } +} + +impl IntoHostValue for Vec { + fn into_host_value(self) -> HostValue { + HostValue::array(self.into_iter().map(IntoHostValue::into_host_value)) + } +} diff --git a/crates/decodal-derive/Cargo.toml b/crates/decodal-derive/Cargo.toml new file mode 100644 index 0000000..a338807 --- /dev/null +++ b/crates/decodal-derive/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "decodal-derive" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +readme.workspace = true +description = "Derive macro for generating Decodal schemas and typed decoders from Rust structs." +keywords = ["decodal", "derive", "proc-macro", "schema"] +categories = ["config", "development-tools::procedural-macro-helpers"] + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1" +quote = "1" +syn = { version = "2", features = ["full"] } + +[dev-dependencies] +decodal = { version = "0.1", path = "../decodal-core" } diff --git a/crates/decodal-derive/src/lib.rs b/crates/decodal-derive/src/lib.rs new file mode 100644 index 0000000..6f1f5a5 --- /dev/null +++ b/crates/decodal-derive/src/lib.rs @@ -0,0 +1,212 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{ + Data, DeriveInput, Expr, Fields, LitStr, Result, Type, parse_macro_input, spanned::Spanned, +}; + +#[proc_macro_derive(Decodal, attributes(decodal))] +pub fn derive_decodal(input: TokenStream) -> TokenStream { + match expand_decodal(parse_macro_input!(input as DeriveInput)) { + Ok(tokens) => tokens.into(), + Err(error) => error.to_compile_error().into(), + } +} + +fn expand_decodal(input: DeriveInput) -> Result { + let name = input.ident; + let fields = match input.data { + Data::Struct(data) => match data.fields { + Fields::Named(fields) => fields.named, + _ => { + return Err(syn::Error::new( + data.struct_token.span(), + "Decodal derive supports structs with named fields only", + )); + } + }, + _ => { + return Err(syn::Error::new( + name.span(), + "Decodal derive supports structs only", + )); + } + }; + + let mut schema_fields = Vec::new(); + let mut decode_fields = Vec::new(); + + for field in fields { + let ident = field.ident.expect("named field"); + let ty = field.ty; + let attrs = FieldAttrs::from_attrs(&field.attrs, &ident.to_string())?; + let path = attrs.rename.clone(); + let schema_value = schema_expr(&ty, &attrs)?; + let decode_value = decode_expr(&ty, &path, &attrs); + + schema_fields.push(quote! { + (#path, { #schema_value }) + }); + decode_fields.push(quote! { + #ident: #decode_value + }); + } + + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + Ok(quote! { + impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause { + fn decodal_schema() -> ::decodal::HostValue { + ::decodal::HostValue::object_from_paths([ + #(#schema_fields),* + ]) + } + } + + impl #impl_generics ::decodal::DecodalDecode for #name #ty_generics #where_clause { + fn decodal_decode(data: &::decodal::Data) -> ::decodal::DecodeResult { + Ok(Self { + #(#decode_fields),* + }) + } + } + }) +} + +#[derive(Default)] +struct FieldAttrs { + rename: String, + default: Option, + constraints: Vec, +} + +impl FieldAttrs { + fn from_attrs(attrs: &[syn::Attribute], fallback_name: &str) -> Result { + let mut output = Self { + rename: fallback_name.to_string(), + ..Self::default() + }; + + for attr in attrs { + if !attr.path().is_ident("decodal") { + continue; + } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("rename") { + let value: LitStr = meta.value()?.parse()?; + output.rename = value.value(); + return Ok(()); + } + if meta.path.is_ident("default") { + if meta.input.peek(syn::Token![=]) { + let expr: Expr = meta.value()?.parse()?; + output.default = Some(DefaultAttr::Expr(expr)); + } else { + output.default = Some(DefaultAttr::Default); + } + return Ok(()); + } + if let Some(kind) = ConstraintKind::from_path(&meta.path) { + let expr: Expr = meta.value()?.parse()?; + output.constraints.push(ConstraintAttr { kind, expr }); + return Ok(()); + } + Err(meta.error("unsupported decodal attribute")) + })?; + } + + Ok(output) + } +} + +enum DefaultAttr { + Default, + Expr(Expr), +} + +struct ConstraintAttr { + kind: ConstraintKind, + expr: Expr, +} + +#[derive(Clone, Copy)] +enum ConstraintKind { + Gt, + Gte, + Lt, + Lte, +} + +impl ConstraintKind { + fn from_path(path: &syn::Path) -> Option { + if path.is_ident("gt") { + Some(Self::Gt) + } else if path.is_ident("gte") { + Some(Self::Gte) + } else if path.is_ident("lt") { + Some(Self::Lt) + } else if path.is_ident("lte") { + Some(Self::Lte) + } else { + None + } + } + + fn method(self) -> syn::Ident { + match self { + Self::Gt => format_ident!("gt"), + Self::Gte => format_ident!("gte"), + Self::Lt => format_ident!("lt"), + Self::Lte => format_ident!("lte"), + } + } +} + +fn schema_expr(ty: &Type, attrs: &FieldAttrs) -> Result { + let mut tokens = quote! { + let mut schema = <#ty as ::decodal::DecodalSchema>::decodal_schema(); + }; + + for constraint in &attrs.constraints { + let method = constraint.kind.method(); + let expr = &constraint.expr; + tokens.extend(quote! { + schema = schema.#method(#expr); + }); + } + + if let Some(default) = &attrs.default { + let default_expr = match default { + DefaultAttr::Default => quote! { <#ty as ::core::default::Default>::default() }, + DefaultAttr::Expr(expr) => quote! { (#expr) }, + }; + tokens.extend(quote! { + schema = schema + .default(::decodal::IntoHostValue::into_host_value(#default_expr)) + .expect("Decodal derive generated a valid default"); + }); + } + + tokens.extend(quote! { schema }); + Ok(tokens) +} + +fn decode_expr(ty: &Type, path: &str, attrs: &FieldAttrs) -> TokenStream2 { + let missing = if let Some(default) = &attrs.default { + match default { + DefaultAttr::Default => quote! { <#ty as ::core::default::Default>::default() }, + DefaultAttr::Expr(expr) => quote! { ::core::convert::Into::into(#expr) }, + } + } else { + quote! { return Err(::decodal::DecodeError::new(#path, "missing field")); } + }; + + quote! { + if let Some(value) = ::decodal::data_at_path(data, #path) { + <#ty as ::decodal::DecodalDecode>::decodal_decode(value) + .map_err(|error| ::decodal::prefix_decode_error(#path, error))? + } else { + #missing + } + } +} diff --git a/crates/decodal-derive/tests/derive.rs b/crates/decodal-derive/tests/derive.rs new file mode 100644 index 0000000..31e9220 --- /dev/null +++ b/crates/decodal-derive/tests/derive.rs @@ -0,0 +1,79 @@ +use decodal::{Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine}; +use decodal_derive::Decodal; + +#[derive(Debug, PartialEq, Decodal)] +struct Service { + name: String, + #[decodal(gt = 443, default = 8443)] + port: i64, + #[decodal(default = true, rename = "feature.enable")] + feature_enabled: bool, + #[decodal(default)] + tags: Vec, +} + +#[test] +fn derives_schema_and_decode() { + let mut engine = Engine::new(EmptyLoader); + engine + .bind_global("Service", Service::decodal_schema()) + .unwrap(); + let module = engine + .add_root_source( + "test", + "test", + r#" + Service & { + name = "api"; + port = 9443; + feature.enable = false; + tags = ["web", "prod"]; + } + "#, + ) + .unwrap(); + let value = engine.eval_module(module).unwrap(); + let data = engine.materialize(&value).unwrap(); + let service = Service::decodal_decode(&data).unwrap(); + assert_eq!( + service, + Service { + name: "api".into(), + port: 9443, + feature_enabled: false, + tags: vec!["web".into(), "prod".into()], + } + ); +} + +#[test] +fn defaults_are_available_in_schema() { + let mut engine = Engine::new(EmptyLoader); + engine + .bind_global("Service", Service::decodal_schema()) + .unwrap(); + let module = engine + .add_root_source( + "test", + "test", + r#" + Service & { + name = "api"; + } + "#, + ) + .unwrap(); + let value = engine.eval_module(module).unwrap(); + let data = engine.materialize(&value).unwrap(); + let service = Service::decodal_decode(&data).unwrap(); + assert_eq!(service.port, 8443); + assert!(service.feature_enabled); + assert!(service.tags.is_empty()); +} + +#[test] +fn decode_reports_field_path() { + let data = Data::Object(vec![]); + let error = Service::decodal_decode(&data).unwrap_err(); + assert_eq!(error.path, "name"); +} diff --git a/crates/decodal-wasm/Cargo.toml b/crates/decodal-wasm/Cargo.toml index e227786..98a0c71 100644 --- a/crates/decodal-wasm/Cargo.toml +++ b/crates/decodal-wasm/Cargo.toml @@ -15,7 +15,7 @@ publish = false crate-type = ["cdylib", "rlib"] [dependencies] -decodal = { version = "0.1.0", path = "../decodal-core" } +decodal = { version = "0.1.1", path = "../decodal-core" } serde_json.workspace = true wasm-bindgen.workspace = true diff --git a/doc/manual/souce/design/embedding-api.md b/doc/manual/souce/design/embedding-api.md index 4bfe70e..eb1f7f2 100644 --- a/doc/manual/souce/design/embedding-api.md +++ b/doc/manual/souce/design/embedding-api.md @@ -17,7 +17,7 @@ let / function env ``` Module top-level bindings shadow prelude bindings. -Primitive type names such as `String`, `Int`, `Float`, and `Bool` are handled before environment lookup, so they are reserved and cannot be shadowed by host 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 @@ -87,6 +87,47 @@ Concrete(Object { 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. diff --git a/doc/manual/souce/development.md b/doc/manual/souce/development.md index fa51c91..667b159 100644 --- a/doc/manual/souce/development.md +++ b/doc/manual/souce/development.md @@ -23,11 +23,13 @@ cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl ## crates.io release The primary crates.io package is `decodal`, which contains the embeddable library. +`decodal-derive` provides optional derive macros for Rust struct integration. Workspace support crates such as `decodal-cli` and `decodal-wasm` are not published for 0.1.0. The project is dual licensed as `MIT OR Apache-2.0`. ```sh cargo publish -p decodal +cargo publish -p decodal-derive ``` Before publishing, run: @@ -37,6 +39,7 @@ cargo fmt --check cargo test cargo check -p decodal --no-default-features cargo publish -p decodal --dry-run +cargo publish -p decodal-derive --dry-run ``` ## Web site and playground diff --git a/doc/manual/souce/language/value/index.md b/doc/manual/souce/language/value/index.md index 6fe89f7..0967dc9 100644 --- a/doc/manual/souce/language/value/index.md +++ b/doc/manual/souce/language/value/index.md @@ -9,9 +9,10 @@ name = String; retry = Int default 3; ratio = Float; enable = Bool default true; +tags = Array; ``` -現在の primitive type は `String`、`Int`、`Float`、`Bool` である。 +現在の primitive type は `String`、`Int`、`Float`、`Bool`、`Array` である。 各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。 primitive type と制約合成の詳細は [制約と default](../constraints-and-defaults.md) も参照する。 diff --git a/package.nix b/package.nix index 8a19bc5..1afa7f0 100644 --- a/package.nix +++ b/package.nix @@ -24,7 +24,7 @@ let in rustPlatform.buildRustPackage { pname = "decodal"; - version = "0.1.0"; + version = "0.1.1"; src = lib.cleanSourceWith { src = srcRoot; diff --git a/site/decodal-site/src/wasm/README.md b/site/decodal-site/src/wasm/README.md index 8db9ac1..d2b7429 100644 --- a/site/decodal-site/src/wasm/README.md +++ b/site/decodal-site/src/wasm/README.md @@ -20,6 +20,35 @@ Embedded hosts should depend on `decodal` and provide imports with a `SourceLoad decodal = "0.1" ``` +## Derive support + +For embedded Rust applications, `decodal-derive` can generate a Decodal schema and typed decoder from a Rust struct. + +```toml +[dependencies] +decodal = "0.1" +decodal-derive = "0.1" +``` + +```rust +use decodal::{DecodalDecode, DecodalSchema, 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, +} +``` + +The derive implements: + +- `DecodalSchema`, which produces a host schema for `Engine::bind_global` +- `DecodalDecode`, which converts materialized `Data` into the Rust struct + ## CLI A standalone CLI is kept in this repository as the `decodal-cli` workspace package. diff --git a/site/decodal-site/src/wasm/decodal_wasm_bg.wasm b/site/decodal-site/src/wasm/decodal_wasm_bg.wasm index 377a9bc33d5a260d0b2f9e111a9ea3a5ba860766..b2f4a2f3fcc9d70d9dce09f6482f31e38722d469 100644 GIT binary patch delta 33351 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 E1y|qJkN^Mx diff --git a/site/decodal-site/src/wasm/package.json b/site/decodal-site/src/wasm/package.json index cf1987a..ec5ba71 100644 --- a/site/decodal-site/src/wasm/package.json +++ b/site/decodal-site/src/wasm/package.json @@ -2,7 +2,7 @@ "name": "decodal-wasm", "type": "module", "description": "WebAssembly wrapper for evaluating Decodal in browser playgrounds.", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT OR Apache-2.0", "repository": { "type": "git",