Skip to content

Commit dbb42c5

Browse files
committed
fix: split .id into .id, .id unpack and .id pack so each is typed exactly
1 parent da1d53f commit dbb42c5

3 files changed

Lines changed: 140 additions & 48 deletions

File tree

‎src/nu/commands/scru128_command.rs‎

Lines changed: 105 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ fn get_string_input(
2222
input: PipelineData,
2323
span: nu_protocol::Span,
2424
) -> Result<String, ShellError> {
25-
if let Some(id) = call.opt::<String>(engine_state, stack, 1)? {
25+
if let Some(id) = call.opt::<String>(engine_state, stack, 0)? {
2626
Ok(id)
2727
} else {
2828
match input {
@@ -44,7 +44,7 @@ fn get_record_input(
4444
input: PipelineData,
4545
span: nu_protocol::Span,
4646
) -> Result<Value, ShellError> {
47-
if let Some(arg) = call.opt::<Value>(engine_state, stack, 1)? {
47+
if let Some(arg) = call.opt::<Value>(engine_state, stack, 0)? {
4848
Ok(arg)
4949
} else {
5050
match input {
@@ -107,26 +107,67 @@ impl Command for Scru128Command {
107107

108108
fn signature(&self) -> Signature {
109109
Signature::build(".id")
110+
.input_output_types(vec![(Type::Nothing, Type::String)])
111+
.category(Category::Experimental)
112+
}
113+
114+
fn description(&self) -> &str {
115+
"Generate a SCRU128 ID"
116+
}
117+
118+
fn extra_description(&self) -> &str {
119+
"Use `.id unpack` to read an id's components, and `.id pack` to build one from them."
120+
}
121+
122+
fn run(
123+
&self,
124+
_engine_state: &EngineState,
125+
_stack: &mut Stack,
126+
call: &Call,
127+
_input: PipelineData,
128+
) -> Result<PipelineData, ShellError> {
129+
let span = call.head;
130+
let result = crate::scru128::generate()
131+
.map_err(|e| scru128_error(format!("Failed to generate ID: {e}"), span))?;
132+
Ok(PipelineData::Value(Value::string(result, span), None))
133+
}
134+
}
135+
136+
#[derive(Clone, Default)]
137+
pub struct Scru128UnpackCommand;
138+
139+
impl Scru128UnpackCommand {
140+
pub fn new() -> Self {
141+
Self
142+
}
143+
}
144+
145+
impl Command for Scru128UnpackCommand {
146+
fn name(&self) -> &str {
147+
".id unpack"
148+
}
149+
150+
fn signature(&self) -> Signature {
151+
// Both input types produce a record, so nu knows the output whether
152+
// the id arrives on the pipeline or as the argument. One command per
153+
// subcommand is what makes that possible: a single `.id` taking the
154+
// subcommand as a positional has one output type per input type, and
155+
// no input has to cover generate, unpack and pack at once.
156+
Signature::build(".id unpack")
110157
.input_output_types(vec![
111-
(Type::Nothing, Type::String),
158+
(Type::Nothing, Type::Record(vec![].into())),
112159
(Type::String, Type::Record(vec![].into())),
113-
(Type::Record(vec![].into()), Type::String),
114160
])
115161
.optional(
116-
"subcommand",
162+
"id",
117163
SyntaxShape::String,
118-
"subcommand: 'unpack' or 'pack'",
119-
)
120-
.optional(
121-
"input",
122-
SyntaxShape::Any,
123-
"input for subcommand (ID string for unpack, record for pack)",
164+
"the id to unpack, if not given on the pipeline",
124165
)
125166
.category(Category::Experimental)
126167
}
127168

128169
fn description(&self) -> &str {
129-
"Generate SCRU128 IDs or manipulate them with unpack/pack operations"
170+
"Unpack a SCRU128 ID into its components"
130171
}
131172

132173
fn run(
@@ -137,45 +178,64 @@ impl Command for Scru128Command {
137178
input: PipelineData,
138179
) -> Result<PipelineData, ShellError> {
139180
let span = call.head;
181+
let id_string = get_string_input(call, engine_state, stack, input, span)?;
182+
let result = crate::scru128::unpack_to_json(&id_string)
183+
.map_err(|e| scru128_error(format!("Failed to unpack ID: {e}"), span))?;
140184

141-
// Check if there's a subcommand
142-
let subcommand: Option<String> = call.opt(engine_state, stack, 0)?;
185+
let nu_value = util::json_to_value(&result, span);
186+
let nu_value = add_when_field(nu_value, span);
143187

144-
match subcommand.as_deref() {
145-
Some("unpack") => {
146-
let id_string = get_string_input(call, engine_state, stack, input, span)?;
147-
let result = crate::scru128::unpack_to_json(&id_string)
148-
.map_err(|e| scru128_error(format!("Failed to unpack ID: {e}"), span))?;
188+
Ok(PipelineData::Value(nu_value, None))
189+
}
190+
}
149191

150-
let nu_value = util::json_to_value(&result, span);
151-
let nu_value = add_when_field(nu_value, span);
192+
#[derive(Clone, Default)]
193+
pub struct Scru128PackCommand;
152194

153-
Ok(PipelineData::Value(nu_value, None))
154-
}
155-
Some("pack") => {
156-
let components = get_record_input(call, engine_state, stack, input, span)?;
157-
let json_value = util::value_to_json(&components);
158-
let json_value = drop_when_field(json_value);
195+
impl Scru128PackCommand {
196+
pub fn new() -> Self {
197+
Self
198+
}
199+
}
159200

160-
let result = crate::scru128::pack_from_json(json_value)
161-
.map_err(|e| scru128_error(format!("Failed to pack components: {e}"), span))?;
201+
impl Command for Scru128PackCommand {
202+
fn name(&self) -> &str {
203+
".id pack"
204+
}
162205

163-
Ok(PipelineData::Value(Value::string(result, span), None))
164-
}
165-
Some(unknown) => Err(ShellError::Generic(
166-
GenericError::new(
167-
"Invalid subcommand",
168-
format!("Unknown subcommand: {unknown}"),
169-
span,
170-
)
171-
.with_help("Available subcommands: unpack, pack"),
172-
)),
173-
None => {
174-
let result = crate::scru128::generate()
175-
.map_err(|e| scru128_error(format!("Failed to generate ID: {e}"), span))?;
206+
fn signature(&self) -> Signature {
207+
Signature::build(".id pack")
208+
.input_output_types(vec![
209+
(Type::Nothing, Type::String),
210+
(Type::Record(vec![].into()), Type::String),
211+
])
212+
.optional(
213+
"components",
214+
SyntaxShape::Record(vec![].into()),
215+
"the components to pack, if not given on the pipeline",
216+
)
217+
.category(Category::Experimental)
218+
}
176219

177-
Ok(PipelineData::Value(Value::string(result, span), None))
178-
}
179-
}
220+
fn description(&self) -> &str {
221+
"Pack SCRU128 components back into an ID"
222+
}
223+
224+
fn run(
225+
&self,
226+
engine_state: &EngineState,
227+
stack: &mut Stack,
228+
call: &Call,
229+
input: PipelineData,
230+
) -> Result<PipelineData, ShellError> {
231+
let span = call.head;
232+
let components = get_record_input(call, engine_state, stack, input, span)?;
233+
let json_value = util::value_to_json(&components);
234+
let json_value = drop_when_field(json_value);
235+
236+
let result = crate::scru128::pack_from_json(json_value)
237+
.map_err(|e| scru128_error(format!("Failed to pack components: {e}"), span))?;
238+
239+
Ok(PipelineData::Value(Value::string(result, span), None))
180240
}
181241
}

‎src/nu/engine.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,8 @@ pub fn add_core_commands(engine: &mut Engine, store: &Store) -> Result<(), Error
432432
Box::new(commands::get_command::GetCommand::new(store.clone())),
433433
Box::new(commands::remove_command::RemoveCommand::new(store.clone())),
434434
Box::new(commands::scru128_command::Scru128Command::new()),
435+
Box::new(commands::scru128_command::Scru128UnpackCommand::new()),
436+
Box::new(commands::scru128_command::Scru128PackCommand::new()),
435437
])
436438
}
437439

‎src/nu/test_commands.rs‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ mod tests {
4848
fn setup_scru128_test_env() -> Engine {
4949
let (_store, mut engine) = setup_test_env();
5050
engine
51-
.add_commands(vec![Box::new(
52-
commands::scru128_command::Scru128Command::new(),
53-
)])
51+
.add_commands(vec![
52+
Box::new(commands::scru128_command::Scru128Command::new()),
53+
Box::new(commands::scru128_command::Scru128UnpackCommand::new()),
54+
Box::new(commands::scru128_command::Scru128PackCommand::new()),
55+
])
5456
.unwrap();
5557
engine
5658
}
@@ -627,6 +629,34 @@ mod tests {
627629
}
628630
}
629631

632+
/// `.id unpack $id` takes its id as an argument, so its pipeline input
633+
/// is nothing. Nu picks an output type at parse time from the input type
634+
/// alone, so the type declared for a nothing input decides what it allows
635+
/// downstream. A wrong one rejects field access before the command runs.
636+
#[test]
637+
fn test_scru128_unpack_argument_form_parses_as_a_record() {
638+
let engine = setup_scru128_test_env();
639+
let id = nu_eval(&engine, PipelineData::empty(), ".id");
640+
let id = id.as_str().unwrap();
641+
642+
// Binding the id to a variable first is what exposes it: with a
643+
// literal, nu infers a type for the whole expression instead.
644+
let ts = nu_eval(
645+
&engine,
646+
PipelineData::empty(),
647+
format!("let id = \"{id}\"; .id unpack $id | get ts_ms"),
648+
);
649+
assert!(ts.as_int().is_ok(), "ts_ms was {ts:?}");
650+
651+
// pack goes the other way, and has the same shape.
652+
let packed = nu_eval(
653+
&engine,
654+
PipelineData::empty(),
655+
format!("let r = (\"{id}\" | .id unpack); .id pack $r"),
656+
);
657+
assert_eq!(packed.as_str().unwrap(), id);
658+
}
659+
630660
#[test]
631661
fn test_scru128_invalid_id() {
632662
let engine = setup_scru128_test_env();

0 commit comments

Comments
 (0)