Skip to main content

shape_runtime/stdlib/
env.rs

1//! Native `env` module for environment variable and system info access.
2//!
3//! Exports: env.get, env.has, env.all, env.args, env.cwd, env.os, env.arch
4//!
5//! Policy gated: requires Env permission at runtime.
6
7use crate::module_exports::{ModuleContext, ModuleExports, ModuleFunction, ModuleParam};
8use shape_value::ValueWord;
9use std::sync::Arc;
10
11/// Create the `env` module with environment variable and system info functions.
12pub fn create_env_module() -> ModuleExports {
13    let mut module = ModuleExports::new("env");
14    module.description = "Environment variables and system information".to_string();
15
16    // env.get(name: string) -> Option<string>
17    module.add_function_with_schema(
18        "get",
19        |args: &[ValueWord], _ctx: &ModuleContext| {
20            let name = args
21                .first()
22                .and_then(|a| a.as_str())
23                .ok_or_else(|| "env.get() requires a variable name string".to_string())?;
24
25            match std::env::var(name) {
26                Ok(val) => Ok(ValueWord::from_some(ValueWord::from_string(Arc::new(val)))),
27                Err(_) => Ok(ValueWord::none()),
28            }
29        },
30        ModuleFunction {
31            description: "Get the value of an environment variable, or none if not set".to_string(),
32            params: vec![ModuleParam {
33                name: "name".to_string(),
34                type_name: "string".to_string(),
35                required: true,
36                description: "Environment variable name".to_string(),
37                ..Default::default()
38            }],
39            return_type: Some("Option<string>".to_string()),
40        },
41    );
42
43    // env.has(name: string) -> bool
44    module.add_function_with_schema(
45        "has",
46        |args: &[ValueWord], _ctx: &ModuleContext| {
47            let name = args
48                .first()
49                .and_then(|a| a.as_str())
50                .ok_or_else(|| "env.has() requires a variable name string".to_string())?;
51
52            Ok(ValueWord::from_bool(std::env::var(name).is_ok()))
53        },
54        ModuleFunction {
55            description: "Check if an environment variable is set".to_string(),
56            params: vec![ModuleParam {
57                name: "name".to_string(),
58                type_name: "string".to_string(),
59                required: true,
60                description: "Environment variable name".to_string(),
61                ..Default::default()
62            }],
63            return_type: Some("bool".to_string()),
64        },
65    );
66
67    // env.all() -> HashMap<string, string>
68    module.add_function_with_schema(
69        "all",
70        |_args: &[ValueWord], _ctx: &ModuleContext| {
71            let vars: Vec<(String, String)> = std::env::vars().collect();
72            let mut keys = Vec::with_capacity(vars.len());
73            let mut values = Vec::with_capacity(vars.len());
74
75            for (k, v) in vars.into_iter() {
76                keys.push(ValueWord::from_string(Arc::new(k)));
77                values.push(ValueWord::from_string(Arc::new(v)));
78            }
79
80            Ok(ValueWord::from_hashmap_pairs(keys, values))
81        },
82        ModuleFunction {
83            description: "Get all environment variables as a HashMap".to_string(),
84            params: vec![],
85            return_type: Some("HashMap<string, string>".to_string()),
86        },
87    );
88
89    // env.args() -> Array<string>
90    module.add_function_with_schema(
91        "args",
92        |_args: &[ValueWord], _ctx: &ModuleContext| {
93            let args: Vec<ValueWord> = std::env::args()
94                .map(|a| ValueWord::from_string(Arc::new(a)))
95                .collect();
96            Ok(ValueWord::from_array(Arc::new(args)))
97        },
98        ModuleFunction {
99            description: "Get command-line arguments as an array of strings".to_string(),
100            params: vec![],
101            return_type: Some("Array<string>".to_string()),
102        },
103    );
104
105    // env.cwd() -> string
106    module.add_function_with_schema(
107        "cwd",
108        |_args: &[ValueWord], _ctx: &ModuleContext| {
109            let cwd = std::env::current_dir().map_err(|e| format!("env.cwd() failed: {}", e))?;
110            let path_str = cwd.to_string_lossy().into_owned();
111            Ok(ValueWord::from_string(Arc::new(path_str)))
112        },
113        ModuleFunction {
114            description: "Get the current working directory".to_string(),
115            params: vec![],
116            return_type: Some("string".to_string()),
117        },
118    );
119
120    // env.os() -> string
121    module.add_function_with_schema(
122        "os",
123        |_args: &[ValueWord], _ctx: &ModuleContext| {
124            Ok(ValueWord::from_string(Arc::new(
125                std::env::consts::OS.to_string(),
126            )))
127        },
128        ModuleFunction {
129            description: "Get the operating system name (e.g. linux, macos, windows)".to_string(),
130            params: vec![],
131            return_type: Some("string".to_string()),
132        },
133    );
134
135    // env.arch() -> string
136    module.add_function_with_schema(
137        "arch",
138        |_args: &[ValueWord], _ctx: &ModuleContext| {
139            Ok(ValueWord::from_string(Arc::new(
140                std::env::consts::ARCH.to_string(),
141            )))
142        },
143        ModuleFunction {
144            description: "Get the CPU architecture (e.g. x86_64, aarch64)".to_string(),
145            params: vec![],
146            return_type: Some("string".to_string()),
147        },
148    );
149
150    module
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    fn s(val: &str) -> ValueWord {
158        ValueWord::from_string(Arc::new(val.to_string()))
159    }
160
161    fn test_ctx() -> crate::module_exports::ModuleContext<'static> {
162        let registry = Box::leak(Box::new(crate::type_schema::TypeSchemaRegistry::new()));
163        crate::module_exports::ModuleContext {
164            schemas: registry,
165            invoke_callable: None,
166            raw_invoker: None,
167            function_hashes: None,
168            vm_state: None,
169            granted_permissions: None,
170            scope_constraints: None,
171            set_pending_resume: None,
172            set_pending_frame_resume: None,
173        }
174    }
175
176    #[test]
177    fn test_env_module_creation() {
178        let module = create_env_module();
179        assert_eq!(module.name, "env");
180        assert!(module.has_export("get"));
181        assert!(module.has_export("has"));
182        assert!(module.has_export("all"));
183        assert!(module.has_export("args"));
184        assert!(module.has_export("cwd"));
185        assert!(module.has_export("os"));
186        assert!(module.has_export("arch"));
187    }
188
189    #[test]
190    fn test_env_get_path() {
191        let module = create_env_module();
192        let ctx = test_ctx();
193        let f = module.get_export("get").unwrap();
194        // PATH should always be set
195        let result = f(&[s("PATH")], &ctx).unwrap();
196        let inner = result.as_some_inner().expect("PATH should be set");
197        assert!(!inner.as_str().unwrap().is_empty());
198    }
199
200    #[test]
201    fn test_env_get_missing() {
202        let module = create_env_module();
203        let ctx = test_ctx();
204        let f = module.get_export("get").unwrap();
205        let result = f(&[s("__SHAPE_NONEXISTENT_VAR_12345__")], &ctx).unwrap();
206        assert!(result.is_none());
207    }
208
209    #[test]
210    fn test_env_get_requires_string() {
211        let module = create_env_module();
212        let ctx = test_ctx();
213        let f = module.get_export("get").unwrap();
214        assert!(f(&[ValueWord::from_f64(42.0)], &ctx).is_err());
215    }
216
217    #[test]
218    fn test_env_has_path() {
219        let module = create_env_module();
220        let ctx = test_ctx();
221        let f = module.get_export("has").unwrap();
222        let result = f(&[s("PATH")], &ctx).unwrap();
223        assert_eq!(result.as_bool(), Some(true));
224    }
225
226    #[test]
227    fn test_env_has_missing() {
228        let module = create_env_module();
229        let ctx = test_ctx();
230        let f = module.get_export("has").unwrap();
231        let result = f(&[s("__SHAPE_NONEXISTENT_VAR_12345__")], &ctx).unwrap();
232        assert_eq!(result.as_bool(), Some(false));
233    }
234
235    #[test]
236    fn test_env_all_returns_hashmap() {
237        let module = create_env_module();
238        let ctx = test_ctx();
239        let f = module.get_export("all").unwrap();
240        let result = f(&[], &ctx).unwrap();
241        let (keys, _values, _index) = result.as_hashmap().expect("should be hashmap");
242        // Should have at least PATH
243        assert!(!keys.is_empty());
244    }
245
246    #[test]
247    fn test_env_args_returns_array() {
248        let module = create_env_module();
249        let ctx = test_ctx();
250        let f = module.get_export("args").unwrap();
251        let result = f(&[], &ctx).unwrap();
252        let arr = result.as_any_array().expect("should be array").to_generic();
253        // At least the binary name
254        assert!(!arr.is_empty());
255    }
256
257    #[test]
258    fn test_env_cwd_returns_string() {
259        let module = create_env_module();
260        let ctx = test_ctx();
261        let f = module.get_export("cwd").unwrap();
262        let result = f(&[], &ctx).unwrap();
263        let cwd = result.as_str().expect("should be string");
264        assert!(!cwd.is_empty());
265    }
266
267    #[test]
268    fn test_env_os_returns_string() {
269        let module = create_env_module();
270        let ctx = test_ctx();
271        let f = module.get_export("os").unwrap();
272        let result = f(&[], &ctx).unwrap();
273        let os = result.as_str().expect("should be string");
274        assert!(!os.is_empty());
275        // Should be one of the known OS values
276        assert!(
277            ["linux", "macos", "windows", "freebsd", "android", "ios"].contains(&os),
278            "unexpected OS: {}",
279            os
280        );
281    }
282
283    #[test]
284    fn test_env_arch_returns_string() {
285        let module = create_env_module();
286        let ctx = test_ctx();
287        let f = module.get_export("arch").unwrap();
288        let result = f(&[], &ctx).unwrap();
289        let arch = result.as_str().expect("should be string");
290        assert!(!arch.is_empty());
291    }
292
293    #[test]
294    fn test_env_schemas() {
295        let module = create_env_module();
296
297        let get_schema = module.get_schema("get").unwrap();
298        assert_eq!(get_schema.params.len(), 1);
299        assert_eq!(get_schema.return_type.as_deref(), Some("Option<string>"));
300
301        let all_schema = module.get_schema("all").unwrap();
302        assert_eq!(all_schema.params.len(), 0);
303
304        let os_schema = module.get_schema("os").unwrap();
305        assert_eq!(os_schema.return_type.as_deref(), Some("string"));
306    }
307}