xs/nu/commands/
append_command.rs1use 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 let user_meta: Option<Value> = call.get_flag(engine_state, stack, "meta")?;
76 let mut final_meta = self.base_meta.clone(); 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); 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}