1use nu_engine::CallExt;
2use nu_protocol::engine::{Call, Command, EngineState, Stack};
3use nu_protocol::{
4 Category, PipelineData, Record, ShellError, Signature, SyntaxShape, Type, Value,
5};
6use serde_json::Value as JsonValue;
7
8use crate::nu::util;
9
10fn scru128_error(msg: String, span: nu_protocol::Span) -> ShellError {
12 ShellError::GenericError {
13 error: "SCRU128 Error".into(),
14 msg,
15 span: Some(span),
16 help: None,
17 inner: vec![],
18 }
19}
20
21#[allow(clippy::result_large_err)]
23fn get_string_input(
24 call: &Call,
25 engine_state: &EngineState,
26 stack: &mut Stack,
27 input: PipelineData,
28 span: nu_protocol::Span,
29) -> Result<String, ShellError> {
30 if let Some(id) = call.opt::<String>(engine_state, stack, 1)? {
31 Ok(id)
32 } else {
33 match input {
34 PipelineData::Value(Value::String { val, .. }, _) => Ok(val),
35 _ => Err(ShellError::GenericError {
36 error: "Missing input".into(),
37 msg: "String required".into(),
38 span: Some(span),
39 help: Some("Provide string as argument or via pipeline".into()),
40 inner: vec![],
41 }),
42 }
43 }
44}
45
46#[allow(clippy::result_large_err)]
48fn get_record_input(
49 call: &Call,
50 engine_state: &EngineState,
51 stack: &mut Stack,
52 input: PipelineData,
53 span: nu_protocol::Span,
54) -> Result<Value, ShellError> {
55 if let Some(arg) = call.opt::<Value>(engine_state, stack, 1)? {
56 Ok(arg)
57 } else {
58 match input {
59 PipelineData::Value(val @ Value::Record { .. }, _) => Ok(val),
60 _ => Err(ShellError::GenericError {
61 error: "Missing input".into(),
62 msg: "Record required".into(),
63 span: Some(span),
64 help: Some("Provide record as argument or via pipeline".into()),
65 inner: vec![],
66 }),
67 }
68 }
69}
70
71fn convert_timestamp_to_datetime(mut nu_value: Value, span: nu_protocol::Span) -> Value {
73 if let Value::Record { val: record, .. } = &mut nu_value {
74 if let Some(Value::Float {
75 val: timestamp_float,
76 ..
77 }) = record.get("timestamp")
78 {
79 let timestamp_ms = (*timestamp_float * 1000.0) as i64;
80 let datetime_value = Value::date(
81 chrono::DateTime::from_timestamp_millis(timestamp_ms)
82 .unwrap_or_else(chrono::Utc::now)
83 .into(),
84 span,
85 );
86 let mut new_record = Record::new();
88 for (key, value) in record.iter() {
89 if key == "timestamp" {
90 new_record.push(key.clone(), datetime_value.clone());
91 } else {
92 new_record.push(key.clone(), value.clone());
93 }
94 }
95 return Value::record(new_record, span);
96 }
97 }
98 nu_value
99}
100
101#[allow(clippy::result_large_err)]
103fn convert_datetime_to_timestamp(
104 mut json_value: JsonValue,
105 span: nu_protocol::Span,
106) -> Result<JsonValue, ShellError> {
107 if let JsonValue::Object(ref mut obj) = json_value {
108 if let Some(JsonValue::String(timestamp_str)) = obj.get("timestamp") {
109 if let Ok(datetime) = chrono::DateTime::parse_from_rfc3339(timestamp_str) {
110 let timestamp_float = datetime.timestamp_millis() as f64 / 1000.0;
111 obj.insert(
112 "timestamp".to_string(),
113 JsonValue::Number(serde_json::Number::from_f64(timestamp_float).ok_or_else(
114 || {
115 scru128_error(
116 "Could not convert datetime to timestamp".to_string(),
117 span,
118 )
119 },
120 )?),
121 );
122 }
123 }
124 }
125 Ok(json_value)
126}
127
128#[derive(Clone, Default)]
129pub struct Scru128Command;
130
131impl Scru128Command {
132 pub fn new() -> Self {
133 Self
134 }
135}
136
137impl Command for Scru128Command {
138 fn name(&self) -> &str {
139 ".id"
140 }
141
142 fn signature(&self) -> Signature {
143 Signature::build(".id")
144 .input_output_types(vec![
145 (Type::Nothing, Type::String),
146 (Type::String, Type::Record(vec![].into())),
147 (Type::Record(vec![].into()), Type::String),
148 ])
149 .optional(
150 "subcommand",
151 SyntaxShape::String,
152 "subcommand: 'unpack' or 'pack'",
153 )
154 .optional(
155 "input",
156 SyntaxShape::Any,
157 "input for subcommand (ID string for unpack, record for pack)",
158 )
159 .category(Category::Experimental)
160 }
161
162 fn description(&self) -> &str {
163 "Generate SCRU128 IDs or manipulate them with unpack/pack operations"
164 }
165
166 fn run(
167 &self,
168 engine_state: &EngineState,
169 stack: &mut Stack,
170 call: &Call,
171 input: PipelineData,
172 ) -> Result<PipelineData, ShellError> {
173 let span = call.head;
174
175 let subcommand: Option<String> = call.opt(engine_state, stack, 0)?;
177
178 match subcommand.as_deref() {
179 Some("unpack") => {
180 let id_string = get_string_input(call, engine_state, stack, input, span)?;
181 let result = crate::scru128::unpack_to_json(&id_string)
182 .map_err(|e| scru128_error(format!("Failed to unpack ID: {e}"), span))?;
183
184 let nu_value = util::json_to_value(&result, span);
185 let nu_value = convert_timestamp_to_datetime(nu_value, span);
186
187 Ok(PipelineData::Value(nu_value, None))
188 }
189 Some("pack") => {
190 let components = get_record_input(call, engine_state, stack, input, span)?;
191 let json_value = util::value_to_json(&components);
192 let json_value = convert_datetime_to_timestamp(json_value, span)?;
193
194 let result = crate::scru128::pack_from_json(json_value)
195 .map_err(|e| scru128_error(format!("Failed to pack components: {e}"), span))?;
196
197 Ok(PipelineData::Value(Value::string(result, span), None))
198 }
199 Some(unknown) => Err(ShellError::GenericError {
200 error: "Invalid subcommand".into(),
201 msg: format!("Unknown subcommand: {unknown}"),
202 span: Some(span),
203 help: Some("Available subcommands: unpack, pack".into()),
204 inner: vec![],
205 }),
206 None => {
207 let result = crate::scru128::generate()
208 .map_err(|e| scru128_error(format!("Failed to generate ID: {e}"), span))?;
209
210 Ok(PipelineData::Value(Value::string(result, span), None))
211 }
212 }
213 }
214}