Skip to main content

awsim_lambda/
lib.rs

1pub mod authz;
2mod error;
3mod executor;
4mod operations;
5pub mod state;
6mod util;
7
8pub use authz::LambdaResourcePolicyLookup;
9
10use std::path::Path;
11use std::sync::Arc;
12
13use async_trait::async_trait;
14use awsim_core::{
15    AccountRegionStore, AwsError, BlobInventory, Body, BodyStore, Protocol, RequestContext,
16    RouteDefinition, ServiceHandler,
17};
18use serde_json::Value;
19use tracing::debug;
20
21use state::{LambdaState, LambdaStateSnapshot};
22
23pub struct LambdaService {
24    store: AccountRegionStore<LambdaState>,
25    body_store: Option<Arc<BodyStore>>,
26}
27
28impl LambdaService {
29    pub fn new() -> Self {
30        Self {
31            store: AccountRegionStore::new(),
32            body_store: None,
33        }
34    }
35
36    pub fn with_data_dir(dir: impl AsRef<Path>) -> Self {
37        Self {
38            store: AccountRegionStore::new(),
39            body_store: Some(Arc::new(BodyStore::new(dir.as_ref().to_path_buf()))),
40        }
41    }
42
43    pub fn with_max_blob_bytes(mut self, bytes: u64) -> Self {
44        if let Some(bs) = self.body_store.take() {
45            let root = bs.root().to_path_buf();
46            self.body_store = Some(Arc::new(BodyStore::new(root).with_max_size(bytes)));
47        }
48        self
49    }
50
51    fn get_state(&self, ctx: &RequestContext) -> Arc<LambdaState> {
52        let state = self.store.get(&ctx.account_id, &ctx.region);
53        if let Some(bs) = &self.body_store {
54            state.set_body_store(Arc::clone(bs));
55        }
56        state
57    }
58
59    pub fn store(&self) -> AccountRegionStore<LambdaState> {
60        self.store.clone()
61    }
62
63    pub fn body_store(&self) -> Option<&Arc<BodyStore>> {
64        self.body_store.as_ref()
65    }
66
67    pub const GROUPS: &'static [&'static str] = &["lambda"];
68
69    fn rebind_bodies(&self) {
70        let Some(bs) = &self.body_store else {
71            return;
72        };
73        for (_, state) in self.store.iter_all() {
74            state.set_body_store(Arc::clone(bs));
75            for mut entry in state.functions.iter_mut() {
76                let name = entry.key().clone();
77                let func = entry.value_mut();
78                if let Ok(path) = bs.blob_path("lambda", &name, "$LATEST") {
79                    func.code = Some(Body::OnDisk(path));
80                }
81                for v in func.versions.iter_mut() {
82                    if let Ok(path) = bs.blob_path("lambda", &name, &v.version) {
83                        v.code = Some(Body::OnDisk(path));
84                    }
85                }
86            }
87        }
88    }
89}
90
91impl Default for LambdaService {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97impl BlobInventory for LambdaService {
98    fn known_blobs(&self) -> Vec<(String, String, String)> {
99        let mut out = Vec::new();
100        for (_, state) in self.store.iter_all() {
101            for func_entry in state.functions.iter() {
102                let name = func_entry.key().clone();
103                out.push(("lambda".to_string(), name.clone(), "$LATEST".to_string()));
104                for v in func_entry.value().versions.iter() {
105                    out.push(("lambda".to_string(), name.clone(), v.version.clone()));
106                }
107            }
108        }
109        out
110    }
111}
112
113#[async_trait]
114impl ServiceHandler for LambdaService {
115    fn service_name(&self) -> &str {
116        "lambda"
117    }
118
119    fn signing_name(&self) -> &str {
120        "lambda"
121    }
122
123    fn protocol(&self) -> Protocol {
124        Protocol::RestJson1
125    }
126
127    fn routes(&self) -> Vec<RouteDefinition> {
128        vec![
129            // Functions
130            RouteDefinition {
131                method: "POST",
132                path_pattern: "/2015-03-31/functions",
133                operation: "CreateFunction",
134                required_query_param: None,
135            },
136            RouteDefinition {
137                method: "GET",
138                path_pattern: "/2015-03-31/functions",
139                operation: "ListFunctions",
140                required_query_param: None,
141            },
142            RouteDefinition {
143                method: "GET",
144                path_pattern: "/2015-03-31/functions/{FunctionName}",
145                operation: "GetFunction",
146                required_query_param: None,
147            },
148            RouteDefinition {
149                method: "DELETE",
150                path_pattern: "/2015-03-31/functions/{FunctionName}",
151                operation: "DeleteFunction",
152                required_query_param: None,
153            },
154            RouteDefinition {
155                method: "GET",
156                path_pattern: "/2015-03-31/functions/{FunctionName}/configuration",
157                operation: "GetFunctionConfiguration",
158                required_query_param: None,
159            },
160            RouteDefinition {
161                method: "PUT",
162                path_pattern: "/2015-03-31/functions/{FunctionName}/code",
163                operation: "UpdateFunctionCode",
164                required_query_param: None,
165            },
166            RouteDefinition {
167                method: "PUT",
168                path_pattern: "/2015-03-31/functions/{FunctionName}/configuration",
169                operation: "UpdateFunctionConfiguration",
170                required_query_param: None,
171            },
172            RouteDefinition {
173                method: "POST",
174                path_pattern: "/2015-03-31/functions/{FunctionName}/invocations",
175                operation: "Invoke",
176                required_query_param: None,
177            },
178            // Versions
179            RouteDefinition {
180                method: "POST",
181                path_pattern: "/2015-03-31/functions/{FunctionName}/versions",
182                operation: "PublishVersion",
183                required_query_param: None,
184            },
185            RouteDefinition {
186                method: "GET",
187                path_pattern: "/2015-03-31/functions/{FunctionName}/versions",
188                operation: "ListVersionsByFunction",
189                required_query_param: None,
190            },
191            // Aliases
192            RouteDefinition {
193                method: "POST",
194                path_pattern: "/2015-03-31/functions/{FunctionName}/aliases",
195                operation: "CreateAlias",
196                required_query_param: None,
197            },
198            RouteDefinition {
199                method: "GET",
200                path_pattern: "/2015-03-31/functions/{FunctionName}/aliases",
201                operation: "ListAliases",
202                required_query_param: None,
203            },
204            RouteDefinition {
205                method: "GET",
206                path_pattern: "/2015-03-31/functions/{FunctionName}/aliases/{Name}",
207                operation: "GetAlias",
208                required_query_param: None,
209            },
210            RouteDefinition {
211                method: "DELETE",
212                path_pattern: "/2015-03-31/functions/{FunctionName}/aliases/{Name}",
213                operation: "DeleteAlias",
214                required_query_param: None,
215            },
216            // Event Source Mappings
217            RouteDefinition {
218                method: "POST",
219                path_pattern: "/2015-03-31/event-source-mappings",
220                operation: "CreateEventSourceMapping",
221                required_query_param: None,
222            },
223            RouteDefinition {
224                method: "GET",
225                path_pattern: "/2015-03-31/event-source-mappings",
226                operation: "ListEventSourceMappings",
227                required_query_param: None,
228            },
229            RouteDefinition {
230                method: "GET",
231                path_pattern: "/2015-03-31/event-source-mappings/{UUID}",
232                operation: "GetEventSourceMapping",
233                required_query_param: None,
234            },
235            RouteDefinition {
236                method: "DELETE",
237                path_pattern: "/2015-03-31/event-source-mappings/{UUID}",
238                operation: "DeleteEventSourceMapping",
239                required_query_param: None,
240            },
241            RouteDefinition {
242                method: "PUT",
243                path_pattern: "/2015-03-31/event-source-mappings/{UUID}",
244                operation: "UpdateEventSourceMapping",
245                required_query_param: None,
246            },
247            // Layers
248            RouteDefinition {
249                method: "POST",
250                path_pattern: "/2018-10-31/layers/{LayerName}/versions",
251                operation: "PublishLayerVersion",
252                required_query_param: None,
253            },
254            RouteDefinition {
255                method: "GET",
256                path_pattern: "/2018-10-31/layers",
257                operation: "ListLayers",
258                required_query_param: None,
259            },
260            RouteDefinition {
261                method: "GET",
262                path_pattern: "/2018-10-31/layers/{LayerName}/versions",
263                operation: "ListLayerVersions",
264                required_query_param: None,
265            },
266            RouteDefinition {
267                method: "DELETE",
268                path_pattern: "/2018-10-31/layers/{LayerName}/versions/{VersionNumber}",
269                operation: "DeleteLayerVersion",
270                required_query_param: None,
271            },
272            // Function URL Configs
273            RouteDefinition {
274                method: "POST",
275                path_pattern: "/2021-10-31/functions/{FunctionName}/url",
276                operation: "CreateFunctionUrlConfig",
277                required_query_param: None,
278            },
279            RouteDefinition {
280                method: "GET",
281                path_pattern: "/2021-10-31/functions/{FunctionName}/url",
282                operation: "GetFunctionUrlConfig",
283                required_query_param: None,
284            },
285            RouteDefinition {
286                method: "DELETE",
287                path_pattern: "/2021-10-31/functions/{FunctionName}/url",
288                operation: "DeleteFunctionUrlConfig",
289                required_query_param: None,
290            },
291            RouteDefinition {
292                method: "GET",
293                path_pattern: "/2021-10-31/functions/{FunctionName}/urls",
294                operation: "ListFunctionUrlConfigs",
295                required_query_param: None,
296            },
297            // Reserved concurrency
298            RouteDefinition {
299                method: "PUT",
300                path_pattern: "/2017-10-31/functions/{FunctionName}/concurrency",
301                operation: "PutFunctionConcurrency",
302                required_query_param: None,
303            },
304            RouteDefinition {
305                method: "GET",
306                path_pattern: "/2019-09-30/functions/{FunctionName}/concurrency",
307                operation: "GetFunctionConcurrency",
308                required_query_param: None,
309            },
310            RouteDefinition {
311                method: "DELETE",
312                path_pattern: "/2017-10-31/functions/{FunctionName}/concurrency",
313                operation: "DeleteFunctionConcurrency",
314                required_query_param: None,
315            },
316            // Provisioned concurrency — single config (Get/Put/Delete) keys
317            // off the Qualifier query param; List omits it.
318            RouteDefinition {
319                method: "PUT",
320                path_pattern: "/2019-09-30/functions/{FunctionName}/provisioned-concurrency",
321                operation: "PutProvisionedConcurrencyConfig",
322                required_query_param: Some("Qualifier"),
323            },
324            RouteDefinition {
325                method: "GET",
326                path_pattern: "/2019-09-30/functions/{FunctionName}/provisioned-concurrency",
327                operation: "GetProvisionedConcurrencyConfig",
328                required_query_param: Some("Qualifier"),
329            },
330            RouteDefinition {
331                method: "DELETE",
332                path_pattern: "/2019-09-30/functions/{FunctionName}/provisioned-concurrency",
333                operation: "DeleteProvisionedConcurrencyConfig",
334                required_query_param: Some("Qualifier"),
335            },
336            RouteDefinition {
337                method: "GET",
338                path_pattern: "/2019-09-30/functions/{FunctionName}/provisioned-concurrency",
339                operation: "ListProvisionedConcurrencyConfigs",
340                required_query_param: None,
341            },
342            // Tags
343            RouteDefinition {
344                method: "GET",
345                path_pattern: "/2017-03-31/tags/{Resource}",
346                operation: "ListTags",
347                required_query_param: None,
348            },
349            RouteDefinition {
350                method: "POST",
351                path_pattern: "/2017-03-31/tags/{Resource}",
352                operation: "TagResource",
353                required_query_param: None,
354            },
355            RouteDefinition {
356                method: "DELETE",
357                path_pattern: "/2017-03-31/tags/{Resource}",
358                operation: "UntagResource",
359                required_query_param: None,
360            },
361            // Policy / Permissions
362            RouteDefinition {
363                method: "GET",
364                path_pattern: "/2015-03-31/functions/{FunctionName}/policy",
365                operation: "GetPolicy",
366                required_query_param: None,
367            },
368            RouteDefinition {
369                method: "POST",
370                path_pattern: "/2015-03-31/functions/{FunctionName}/policy",
371                operation: "AddPermission",
372                required_query_param: None,
373            },
374            RouteDefinition {
375                method: "DELETE",
376                path_pattern: "/2015-03-31/functions/{FunctionName}/policy/{StatementId}",
377                operation: "RemovePermission",
378                required_query_param: None,
379            },
380            // Account Settings
381            RouteDefinition {
382                method: "GET",
383                path_pattern: "/2016-08-19/account-settings",
384                operation: "GetAccountSettings",
385                required_query_param: None,
386            },
387            // Event Invoke Configs
388            RouteDefinition {
389                method: "PUT",
390                path_pattern: "/2019-09-25/functions/{FunctionName}/event-invoke-config",
391                operation: "PutFunctionEventInvokeConfig",
392                required_query_param: None,
393            },
394            RouteDefinition {
395                method: "GET",
396                path_pattern: "/2019-09-25/functions/{FunctionName}/event-invoke-config",
397                operation: "GetFunctionEventInvokeConfig",
398                required_query_param: None,
399            },
400            RouteDefinition {
401                method: "DELETE",
402                path_pattern: "/2019-09-25/functions/{FunctionName}/event-invoke-config",
403                operation: "DeleteFunctionEventInvokeConfig",
404                required_query_param: None,
405            },
406            RouteDefinition {
407                method: "POST",
408                path_pattern: "/2019-09-25/functions/{FunctionName}/event-invoke-config",
409                operation: "UpdateFunctionEventInvokeConfig",
410                required_query_param: None,
411            },
412            RouteDefinition {
413                method: "GET",
414                path_pattern: "/2019-09-25/functions/{FunctionName}/event-invoke-config/list",
415                operation: "ListFunctionEventInvokeConfigs",
416                required_query_param: None,
417            },
418        ]
419    }
420
421    async fn handle(
422        &self,
423        operation: &str,
424        input: Value,
425        ctx: &RequestContext,
426    ) -> Result<Value, AwsError> {
427        debug!(operation, "Lambda request");
428        let state = self.get_state(ctx);
429
430        match operation {
431            // Functions
432            "CreateFunction" => operations::functions::create_function(&state, &input, ctx),
433            "GetFunction" => operations::functions::get_function(&state, &input, ctx),
434            "GetFunctionConfiguration" => {
435                operations::functions::get_function_configuration(&state, &input, ctx)
436            }
437            "DeleteFunction" => operations::functions::delete_function(&state, &input),
438            "ListFunctions" => operations::functions::list_functions(&state, &input, ctx),
439            "UpdateFunctionCode" => {
440                operations::functions::update_function_code(&state, &input, ctx)
441            }
442            "UpdateFunctionConfiguration" => {
443                operations::functions::update_function_configuration(&state, &input, ctx)
444            }
445
446            // Invocations
447            "Invoke" => operations::invocations::invoke(&state, &input, ctx),
448
449            // Versions
450            "PublishVersion" => operations::versions::publish_version(&state, &input, ctx),
451            "ListVersionsByFunction" => {
452                operations::versions::list_versions_by_function(&state, &input, ctx)
453            }
454
455            // Aliases
456            "CreateAlias" => operations::aliases::create_alias(&state, &input, ctx),
457            "GetAlias" => operations::aliases::get_alias(&state, &input, ctx),
458            "DeleteAlias" => operations::aliases::delete_alias(&state, &input, ctx),
459            "ListAliases" => operations::aliases::list_aliases(&state, &input, ctx),
460
461            // Event Source Mappings
462            "CreateEventSourceMapping" => {
463                operations::event_source_mappings::create_event_source_mapping(&state, &input, ctx)
464            }
465            "GetEventSourceMapping" => {
466                operations::event_source_mappings::get_event_source_mapping(&state, &input, ctx)
467            }
468            "DeleteEventSourceMapping" => {
469                operations::event_source_mappings::delete_event_source_mapping(&state, &input, ctx)
470            }
471            "ListEventSourceMappings" => {
472                operations::event_source_mappings::list_event_source_mappings(&state, &input, ctx)
473            }
474            "UpdateEventSourceMapping" => {
475                operations::event_source_mappings::update_event_source_mapping(&state, &input, ctx)
476            }
477
478            // Layers
479            "PublishLayerVersion" => operations::layers::publish_layer_version(&state, &input, ctx),
480            "ListLayers" => operations::layers::list_layers(&state, &input, ctx),
481            "ListLayerVersions" => operations::layers::list_layer_versions(&state, &input, ctx),
482            "DeleteLayerVersion" => operations::layers::delete_layer_version(&state, &input, ctx),
483
484            // Function URL Configs
485            "CreateFunctionUrlConfig" => {
486                operations::url_configs::create_function_url_config(&state, &input, ctx)
487            }
488            "GetFunctionUrlConfig" => {
489                operations::url_configs::get_function_url_config(&state, &input, ctx)
490            }
491            "DeleteFunctionUrlConfig" => {
492                operations::url_configs::delete_function_url_config(&state, &input, ctx)
493            }
494            "ListFunctionUrlConfigs" => {
495                operations::url_configs::list_function_url_configs(&state, &input, ctx)
496            }
497
498            // Tags
499            "TagResource" => operations::tags::tag_resource(&state, &input, ctx),
500            "UntagResource" => operations::tags::untag_resource(&state, &input, ctx),
501            "ListTags" => operations::tags::list_tags(&state, &input, ctx),
502
503            // Policy / Permissions
504            "GetPolicy" => operations::permissions::get_policy(&state, &input, ctx),
505            "AddPermission" => operations::permissions::add_permission(&state, &input, ctx),
506            "RemovePermission" => operations::permissions::remove_permission(&state, &input, ctx),
507
508            // Account Settings
509            "GetAccountSettings" => {
510                operations::permissions::get_account_settings(&state, &input, ctx)
511            }
512
513            // Event Invoke Configs
514            "PutFunctionEventInvokeConfig" => {
515                operations::event_invoke_configs::put_function_event_invoke_config(
516                    &state, &input, ctx,
517                )
518            }
519            "GetFunctionEventInvokeConfig" => {
520                operations::event_invoke_configs::get_function_event_invoke_config(
521                    &state, &input, ctx,
522                )
523            }
524            "UpdateFunctionEventInvokeConfig" => {
525                operations::event_invoke_configs::update_function_event_invoke_config(
526                    &state, &input, ctx,
527                )
528            }
529            "DeleteFunctionEventInvokeConfig" => {
530                operations::event_invoke_configs::delete_function_event_invoke_config(
531                    &state, &input, ctx,
532                )
533            }
534            "ListFunctionEventInvokeConfigs" => {
535                operations::event_invoke_configs::list_function_event_invoke_configs(
536                    &state, &input, ctx,
537                )
538            }
539
540            // Reserved + provisioned concurrency
541            "PutFunctionConcurrency" => {
542                operations::concurrency::put_function_concurrency(&state, &input, ctx)
543            }
544            "GetFunctionConcurrency" => {
545                operations::concurrency::get_function_concurrency(&state, &input, ctx)
546            }
547            "DeleteFunctionConcurrency" => {
548                operations::concurrency::delete_function_concurrency(&state, &input, ctx)
549            }
550            "PutProvisionedConcurrencyConfig" => {
551                operations::concurrency::put_provisioned_concurrency_config(&state, &input, ctx)
552            }
553            "GetProvisionedConcurrencyConfig" => {
554                operations::concurrency::get_provisioned_concurrency_config(&state, &input, ctx)
555            }
556            "DeleteProvisionedConcurrencyConfig" => {
557                operations::concurrency::delete_provisioned_concurrency_config(&state, &input, ctx)
558            }
559            "ListProvisionedConcurrencyConfigs" => {
560                operations::concurrency::list_provisioned_concurrency_configs(&state, &input, ctx)
561            }
562
563            _ => Err(AwsError::unknown_operation(operation)),
564        }
565    }
566
567    fn snapshot(&self) -> Option<Vec<u8>> {
568        self.store.snapshot_to_bytes()
569    }
570
571    fn restore(&self, data: &[u8]) -> Result<(), String> {
572        use awsim_core::Snapshottable;
573        use state::LambdaRegionSnapshot;
574
575        if let Ok(()) = self.store.restore_from_bytes(data) {
576            self.rebind_bodies();
577            return Ok(());
578        }
579
580        let legacy: LambdaStateSnapshot =
581            serde_json::from_slice(data).map_err(|e| e.to_string())?;
582        let mut by_region: std::collections::HashMap<(String, String), Vec<_>> =
583            std::collections::HashMap::new();
584        for fs in legacy.functions {
585            by_region
586                .entry((fs.account_id.clone(), fs.region.clone()))
587                .or_default()
588                .push(fs);
589        }
590        self.store.clear();
591        for ((account_id, region), functions) in by_region {
592            let snap = LambdaRegionSnapshot {
593                account_id: account_id.clone(),
594                region: region.clone(),
595                functions,
596            };
597            let (acct, reg, state) = LambdaState::from_snapshot(snap);
598            self.store.set(&acct, &reg, state);
599        }
600        self.rebind_bodies();
601        Ok(())
602    }
603
604    fn iam_action(&self, operation: &str) -> Option<String> {
605        match operation {
606            "CreateFunction"
607            | "GetFunction"
608            | "GetFunctionConfiguration"
609            | "DeleteFunction"
610            | "ListFunctions"
611            | "UpdateFunctionCode"
612            | "UpdateFunctionConfiguration"
613            | "Invoke"
614            | "InvokeFunction"
615            | "InvokeAsync"
616            | "PublishVersion"
617            | "ListVersionsByFunction"
618            | "CreateAlias"
619            | "GetAlias"
620            | "DeleteAlias"
621            | "ListAliases"
622            | "UpdateAlias"
623            | "CreateEventSourceMapping"
624            | "GetEventSourceMapping"
625            | "DeleteEventSourceMapping"
626            | "ListEventSourceMappings"
627            | "UpdateEventSourceMapping"
628            | "PublishLayerVersion"
629            | "ListLayers"
630            | "ListLayerVersions"
631            | "DeleteLayerVersion"
632            | "GetLayerVersion"
633            | "CreateFunctionUrlConfig"
634            | "GetFunctionUrlConfig"
635            | "DeleteFunctionUrlConfig"
636            | "ListFunctionUrlConfigs"
637            | "UpdateFunctionUrlConfig"
638            | "TagResource"
639            | "UntagResource"
640            | "ListTags"
641            | "GetPolicy"
642            | "AddPermission"
643            | "RemovePermission"
644            | "GetAccountSettings"
645            | "PutFunctionEventInvokeConfig"
646            | "GetFunctionEventInvokeConfig"
647            | "UpdateFunctionEventInvokeConfig"
648            | "DeleteFunctionEventInvokeConfig"
649            | "ListFunctionEventInvokeConfigs"
650            | "PutFunctionConcurrency"
651            | "GetFunctionConcurrency"
652            | "DeleteFunctionConcurrency"
653            | "PutProvisionedConcurrencyConfig"
654            | "GetProvisionedConcurrencyConfig"
655            | "DeleteProvisionedConcurrencyConfig"
656            | "ListProvisionedConcurrencyConfigs" => Some(format!("lambda:{operation}")),
657            _ => None,
658        }
659    }
660
661    fn iam_resource(&self, operation: &str, input: &Value, ctx: &RequestContext) -> Option<String> {
662        let prefix = format!("arn:aws:lambda:{}:{}", ctx.region, ctx.account_id);
663        match operation {
664            "ListFunctions"
665            | "ListEventSourceMappings"
666            | "ListLayers"
667            | "GetAccountSettings"
668            | "CreateFunction"
669            | "CreateEventSourceMapping" => Some("*".to_string()),
670            "TagResource" | "UntagResource" | "ListTags" => input
671                .get("Resource")
672                .and_then(|v| v.as_str())
673                .map(|s| s.to_string()),
674            "GetEventSourceMapping" | "DeleteEventSourceMapping" | "UpdateEventSourceMapping" => {
675                input
676                    .get("UUID")
677                    .and_then(|v| v.as_str())
678                    .map(|uuid| format!("{prefix}:event-source-mapping:{uuid}"))
679            }
680            "PublishLayerVersion" | "ListLayerVersions" => input
681                .get("LayerName")
682                .and_then(|v| v.as_str())
683                .map(|name| format!("{prefix}:layer:{name}")),
684            "GetLayerVersion" | "DeleteLayerVersion" => {
685                let name = input.get("LayerName").and_then(|v| v.as_str())?;
686                let version = input
687                    .get("VersionNumber")
688                    .and_then(|v| v.as_i64())
689                    .unwrap_or(0);
690                Some(format!("{prefix}:layer:{name}:{version}"))
691            }
692            _ => {
693                let name = input.get("FunctionName").and_then(|v| v.as_str())?;
694                if name.starts_with("arn:") {
695                    Some(name.to_string())
696                } else {
697                    Some(format!("{prefix}:function:{name}"))
698                }
699            }
700        }
701    }
702}