diff --git a/Cargo.toml b/Cargo.toml index 81cbe637..39532b69 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ no_closure = [] # no automatic sharing and capture of anonymous functions to no_module = [] # no modules internals = [] # expose internal data structures unicode-xid-ident = ["unicode-xid"] # allow Unicode Standard Annex #31 for identifiers. +metadata = [ "serde", "serde_json"] # compiling for no-std no_std = [ "smallvec/union", "num-traits/libm", "hashbrown", "core-error", "libm", "ahash" ] @@ -86,6 +87,12 @@ default_features = false features = ["derive", "alloc"] optional = true +[dependencies.serde_json] +version = "1.0.60" +default_features = false +features = ["alloc"] +optional = true + [dependencies.unicode-xid] version = "0.2.1" default_features = false diff --git a/doc/src/links.md b/doc/src/links.md index 115c8890..3eee2ae7 100644 --- a/doc/src/links.md +++ b/doc/src/links.md @@ -13,6 +13,7 @@ [`no_closure`]: {{rootUrl}}/start/features.md [`no_std`]: {{rootUrl}}/start/features.md [`no-std`]: {{rootUrl}}/start/features.md +[`metadata`]: {{rootUrl}}/start/features.md [`internals`]: {{rootUrl}}/start/features.md [`unicode-xid-ident`]: {{rootUrl}}/start/features.md diff --git a/doc/src/start/features.md b/doc/src/start/features.md index 85a13168..ee7c8fa9 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -26,8 +26,9 @@ more control over what a script can (or cannot) do. | `no_module` | no | disables loading external [modules] | | `no_closure` | no | disables [capturing][automatic currying] external variables in [anonymous functions] to simulate _closures_, or [capturing the calling scope]({{rootUrl}}/language/fn-capture.md) in function calls | | `no_std` | no | builds for `no-std` (implies `no_closure`). Notice that additional dependencies will be pulled in to replace `std` features | -| `serde` | yes | enables serialization/deserialization via `serde`. Notice that the [`serde`](https://crates.io/crates/serde) crate will be pulled in together with its dependencies | +| `serde` | yes | enables serialization/deserialization via `serde` (requires the [`serde`](https://crates.io/crates/serde) crate) | | `unicode-xid-ident` | no | allows [Unicode Standard Annex #31](http://www.unicode.org/reports/tr31/) as identifiers | +| `metadata` | yes | enables exporting functions metadata to JSON format (requires the [`serde`](https://crates.io/crates/serde) and [`serde_json`](https://crates.io/crates/serde_json) crates) | | `internals` | yes | exposes internal data structures (e.g. [`AST`] nodes). Beware that Rhai internals are volatile and may change from version to version | diff --git a/src/serde_impl/metadata.rs b/src/serde_impl/metadata.rs new file mode 100644 index 00000000..0d6b710a --- /dev/null +++ b/src/serde_impl/metadata.rs @@ -0,0 +1,208 @@ +use crate::stdlib::{ + collections::BTreeMap, + string::{String, ToString}, + vec, + vec::Vec, +}; +use crate::{Engine, AST}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +enum FnType { + Script, + Native, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +enum FnNamespace { + Global, + Internal, +} + +impl From for FnNamespace { + fn from(value: crate::FnNamespace) -> Self { + match value { + crate::FnNamespace::Global => Self::Global, + crate::FnNamespace::Internal => Self::Internal, + } + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +enum FnAccess { + Public, + Private, +} + +impl From for FnAccess { + fn from(value: crate::FnAccess) -> Self { + match value { + crate::FnAccess::Public => Self::Public, + crate::FnAccess::Private => Self::Private, + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct FnParam { + pub name: String, + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub typ: Option, +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct FnMetadata { + pub namespace: FnNamespace, + pub access: FnAccess, + pub name: String, + #[serde(rename = "type")] + pub typ: FnType, + pub num_params: usize, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub params: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub return_type: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub doc_comments: Option>, +} + +impl From<&crate::module::FuncInfo> for FnMetadata { + fn from(info: &crate::module::FuncInfo) -> Self { + Self { + namespace: info.namespace.into(), + access: info.access.into(), + name: info.name.to_string(), + typ: if info.func.is_script() { + FnType::Script + } else { + FnType::Native + }, + num_params: info.params, + params: if let Some(ref names) = info.param_names { + names + .iter() + .take(info.params) + .map(|s| { + let mut seg = s.splitn(2, ':'); + let name = seg + .next() + .map(|s| s.trim().to_string()) + .unwrap_or("_".to_string()); + let typ = seg.next().map(|s| s.trim().to_string()); + FnParam { name, typ } + }) + .collect() + } else { + vec![] + }, + return_type: if let Some(ref names) = info.param_names { + names + .last() + .map(|s| s.to_string()) + .or_else(|| Some("()".to_string())) + } else { + None + }, + doc_comments: if info.func.is_script() { + Some(info.func.get_fn_def().comments.clone()) + } else { + None + }, + } + } +} + +impl From> for FnMetadata { + fn from(info: crate::ScriptFnMetadata) -> Self { + Self { + namespace: FnNamespace::Global, + access: info.access.into(), + name: info.name.to_string(), + typ: FnType::Script, + num_params: info.params.len(), + params: info + .params + .iter() + .map(|s| FnParam { + name: s.to_string(), + typ: Some("Dynamic".to_string()), + }) + .collect(), + return_type: Some("Dynamic".to_string()), + doc_comments: if info.comments.is_empty() { + None + } else { + Some(info.comments.iter().map(|s| s.to_string()).collect()) + }, + } + } +} + +#[derive(Debug, Clone, Default, Serialize)] +#[serde(rename_all = "camelCase")] +struct ModuleMetadata { + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + pub modules: BTreeMap, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub functions: Vec, +} + +impl From<&crate::Module> for ModuleMetadata { + fn from(module: &crate::Module) -> Self { + Self { + modules: module + .iter_sub_modules() + .map(|(name, m)| (name.to_string(), m.as_ref().into())) + .collect(), + functions: module.iter_fn().map(|f| f.into()).collect(), + } + } +} + +#[cfg(feature = "serde")] +impl Engine { + /// Generate a list of all functions (including those defined in an [`AST`][crate::AST], if provided) + /// in JSON format. Available only under the `metadata` feature. + /// + /// Functions from the following sources are included: + /// 1) Functions defined in an [`AST`][crate::AST] (if provided) + /// 2) Functions registered into the global namespace + /// 3) Functions in registered sub-modules + /// 4) Functions in packages (optional) + pub fn gen_fn_metadata_to_json( + &self, + ast: Option<&AST>, + include_packages: bool, + ) -> serde_json::Result { + let mut global: ModuleMetadata = Default::default(); + + if include_packages { + self.packages + .iter() + .flat_map(|m| m.iter_fn().map(|f| f.into())) + .for_each(|info| global.functions.push(info)); + } + + self.global_sub_modules.iter().for_each(|(name, m)| { + global.modules.insert(name.to_string(), m.as_ref().into()); + }); + + self.global_namespace + .iter_fn() + .map(|f| f.into()) + .for_each(|info| global.functions.push(info)); + + if let Some(ast) = ast { + ast.iter_functions() + .map(|f| f.into()) + .for_each(|info| global.functions.push(info)); + } + + serde_json::to_string_pretty(&global) + } +} diff --git a/src/serde_impl/mod.rs b/src/serde_impl/mod.rs index da853425..d38105a1 100644 --- a/src/serde_impl/mod.rs +++ b/src/serde_impl/mod.rs @@ -3,3 +3,6 @@ pub mod de; pub mod ser; mod str; + +#[cfg(feature = "metadata")] +pub mod metadata;