xs/nu/commands/
append_command.rs

1use nu_engine::CallExt;
2use nu_protocol::engine::{Call, Command, EngineState, Stack};
3use nu_protocol::{Category, PipelineData, ShellError, Signature, SyntaxShape, Type, Value};
4
5use serde_json::Value as JsonValue;
6
7use crate::nu::util;
8use crate::store::{Frame, Store, TTL};
9
10#[derive(Clone)]
11pub struct AppendCommand {
12    store: Store,
13    context_id: scru128::Scru128Id,
14    base_meta: JsonValue,
15}
16
17impl AppendCommand {
18    pub fn new(store: Store, context_id: scru128::Scru128Id, base_meta: JsonValue) -> Self {
19        Self {
20            store,
21            context_id,
22            base_meta,
23        }
24    }
25}
26
27impl Command for AppendCommand {
28    fn name(&self) -> &str {
29        ".append"
30    }
31
32    fn signature(&self) -> Signature {
33        Signature::build(".append")
34            .input_output_types(vec![(Type::Any, Type::Any)])
35            .required("topic", SyntaxShape::String, "this clip's topic")
36            .named(
37                "meta",
38                SyntaxShape::Record(vec![]),
39                "arbitrary metadata",
40                None,
41            )
42            .named(
43                "ttl",
44                SyntaxShape::String,
45                r#"TTL specification: 'forever', 'ephemeral', 'time:<milliseconds>', or 'head:<n>'"#,
46                None,
47            )
48            .named(
49                "context",
50                SyntaxShape::String,
51                "context ID (defaults to system context)",
52                None,
53            )
54            .category(Category::Experimental)
55    }
56
57    fn description(&self) -> &str {
58        "Writes its input to the CAS and then appends a frame with a hash of this content to the given topic on the stream."
59    }
60
61    fn run(
62        &self,
63        engine_state: &EngineState,
64        stack: &mut Stack,
65        call: &Call,
66        input: PipelineData,
67    ) -> Result<PipelineData, ShellError> {
68        let span = call.head;
69
70        let store = self.store.clone();
71
72        let topic: String = call.req(engine_state, stack, 0)?;
73
74        // Get user-supplied metadata and convert to JSON
75        let user_meta: Option<Value> = call.get_flag(engine_state, stack, "meta")?;
76        let mut final_meta = self.base_meta.clone(); // Start with base metadata
77
78        // Merge user metadata if provided
79        if let Some(user_value) = user_meta {
80            let user_json = util::value_to_json(&user_value);
81            if let JsonValue::Object(mut base_obj) = final_meta {
82                if let JsonValue::Object(user_obj) = user_json {
83                    base_obj.extend(user_obj); // Merge user metadata into base
84                    final_meta = JsonValue::Object(base_obj);
85                } else {
86                    return Err(ShellError::TypeMismatch {
87                        err_message: "Meta must be a record".to_string(),
88                        span: call.span(),
89                    });
90                }
91            }
92        }
93
94        let ttl: Option<String> = call.get_flag(engine_state, stack, "ttl")?;
95        let ttl = match ttl {
96            Some(ttl_str) => Some(TTL::from_query(Some(&format!("ttl={ttl_str}"))).map_err(
97                |e| ShellError::TypeMismatch {
98                    err_message: format!("Invalid TTL value: {ttl_str}. {e}"),
99                    span: call.span(),
100                },
101            )?),
102            None => None,
103        };
104
105        let hash = util::write_pipeline_to_cas(input, &store, span).map_err(|boxed| *boxed)?;
106        let context_str: Option<String> = call.get_flag(engine_state, stack, "context")?;
107        let context_id = context_str
108            .map(|ctx| ctx.parse::<scru128::Scru128Id>())
109            .transpose()
110            .map_err(|e| ShellError::GenericError {
111                error: "Invalid context ID".into(),
112                msg: e.to_string(),
113                span: Some(call.head),
114                help: None,
115                inner: vec![],
116            })?
117            .unwrap_or(self.context_id);
118
119        let frame = store.append(
120            Frame::builder(topic, context_id)
121                .maybe_hash(hash)
122                .meta(final_meta)
123                .maybe_ttl(ttl)
124                .build(),
125        )?;
126
127        Ok(PipelineData::Value(
128            util::frame_to_value(&frame, span),
129            None,
130        ))
131    }
132}