Skip to main content

rustvello_proto/
identifiers.rs

1use std::fmt;
2use std::str::FromStr;
3use std::sync::Arc;
4
5use serde::{Deserialize, Serialize};
6
7/// Uniquely identifies a task definition (language + module path + function name).
8///
9/// Mirrors pynenc's `TaskId` which is computed as `module.function_name`.
10/// The optional `language` field enables cross-language task routing:
11/// when empty, the task is local (same language); when set (e.g. "rust",
12/// "python"), it identifies a foreign task for per-language queue routing.
13///
14/// # Format
15///
16/// The canonical string representation is:
17/// - Local tasks: `module.name`
18/// - Foreign tasks: `language::module.name`
19///
20/// The `name` field must not contain dots to ensure lossless round-tripping
21/// through `Display`/`FromStr`.
22///
23/// # Representation
24///
25/// Internally, all three components are packed into a single `Arc<str>`
26/// allocation with byte offsets for O(1) field access. Cloning a `TaskId`
27/// is a single atomic increment with zero heap allocation.
28#[derive(Clone, PartialEq, Eq, Hash)]
29pub struct TaskId {
30    /// Packed string: `language ++ module ++ name` (concatenated, no separators).
31    packed: Arc<str>,
32    /// Byte offset where `language` ends (and `module` begins).
33    lang_end: u16,
34    /// Byte offset where `module` ends (and `name` begins).
35    module_end: u16,
36}
37
38impl TaskId {
39    /// Build a packed `TaskId` from its three components (no validation).
40    ///
41    /// This is the single internal path that all constructors and
42    /// deserialization go through.
43    fn from_parts(language: &str, module: &str, name: &str) -> Self {
44        let lang_end = language.len();
45        let module_end = lang_end + module.len();
46        // Use assert! (not debug_assert!) so truncation to u16 is caught in
47        // release builds too. Silent truncation would corrupt every field accessor.
48        assert!(
49            lang_end <= u16::MAX as usize,
50            "TaskId language field exceeds u16::MAX bytes ({lang_end} bytes)"
51        );
52        assert!(
53            module_end <= u16::MAX as usize,
54            "TaskId language + module exceeds u16::MAX bytes ({module_end} bytes)"
55        );
56        let mut buf = String::with_capacity(module_end + name.len());
57        buf.push_str(language);
58        buf.push_str(module);
59        buf.push_str(name);
60        Self {
61            packed: Arc::from(buf.as_str()),
62            lang_end: lang_end as u16,
63            module_end: module_end as u16,
64        }
65    }
66
67    /// Create a local (same-language) task ID — backward compatible.
68    ///
69    /// # Panics
70    /// Panics if `name` contains a dot — dots break the `Display`/`FromStr`
71    /// round-trip since `module.name` uses `.` as the separator.
72    ///
73    /// Use [`try_new`](Self::try_new) for user-supplied input at API boundaries.
74    pub fn new(module: impl Into<String>, name: impl Into<String>) -> Self {
75        let name = name.into();
76        assert!(
77            !name.contains('.'),
78            "TaskId name must not contain dots (breaks Display/FromStr round-trip): {name:?}"
79        );
80        let module = module.into();
81        Self::from_parts("", &module, &name)
82    }
83
84    /// Fallible constructor for user-supplied input at API boundaries.
85    ///
86    /// Returns `Err` if `name` contains a dot.
87    pub fn try_new(
88        module: impl Into<String>,
89        name: impl Into<String>,
90    ) -> Result<Self, ParseTaskIdError> {
91        let name = name.into();
92        if name.contains('.') {
93            return Err(ParseTaskIdError(format!(
94                "TaskId name must not contain dots (breaks Display/FromStr round-trip): {name:?}"
95            )));
96        }
97        let module = module.into();
98        Ok(Self::from_parts("", &module, &name))
99    }
100
101    /// Create a foreign (cross-language) task ID.
102    ///
103    /// # Panics
104    /// Panics if `language` is empty — use [`TaskId::new`] for local tasks.
105    ///
106    /// Use [`try_foreign`](Self::try_foreign) for user-supplied input at API boundaries.
107    pub fn foreign(
108        language: impl Into<String>,
109        module: impl Into<String>,
110        name: impl Into<String>,
111    ) -> Self {
112        let language = language.into();
113        let name = name.into();
114        assert!(
115            !language.is_empty(),
116            "TaskId::foreign called with empty language; use TaskId::new for local tasks"
117        );
118        assert!(
119            !name.contains('.'),
120            "TaskId name must not contain dots (breaks Display/FromStr round-trip): {name:?}"
121        );
122        let module = module.into();
123        Self::from_parts(&language, &module, &name)
124    }
125
126    /// Fallible constructor for foreign task IDs from user-supplied input.
127    ///
128    /// Returns `Err` if `language` is empty or `name` contains a dot.
129    pub fn try_foreign(
130        language: impl Into<String>,
131        module: impl Into<String>,
132        name: impl Into<String>,
133    ) -> Result<Self, ParseTaskIdError> {
134        let language = language.into();
135        let name = name.into();
136        if language.is_empty() {
137            return Err(ParseTaskIdError(
138                "TaskId::try_foreign called with empty language; use try_new for local tasks"
139                    .to_owned(),
140            ));
141        }
142        if name.contains('.') {
143            return Err(ParseTaskIdError(format!(
144                "TaskId name must not contain dots (breaks Display/FromStr round-trip): {name:?}"
145            )));
146        }
147        let module = module.into();
148        Ok(Self::from_parts(&language, &module, &name))
149    }
150
151    /// Whether this references a task in a different language.
152    pub fn is_foreign(&self) -> bool {
153        self.lang_end > 0
154    }
155
156    /// Get the language identifier (empty for local tasks).
157    pub fn language(&self) -> &str {
158        &self.packed[..self.lang_end as usize]
159    }
160
161    /// Get the module path.
162    pub fn module(&self) -> &str {
163        &self.packed[self.lang_end as usize..self.module_end as usize]
164    }
165
166    /// Get the task function name.
167    pub fn name(&self) -> &str {
168        &self.packed[self.module_end as usize..]
169    }
170
171    /// Config file key for this task (dots replaced with underscores).
172    ///
173    /// Mirrors pynenc's `TaskId.config_key` used for TOML/YAML lookups.
174    pub fn config_key(&self) -> String {
175        format!("{}_{}", self.module().replace('.', "_"), self.name())
176    }
177}
178
179impl fmt::Debug for TaskId {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        f.debug_struct("TaskId")
182            .field("language", &self.language())
183            .field("module", &self.module())
184            .field("name", &self.name())
185            .finish()
186    }
187}
188
189impl Serialize for TaskId {
190    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
191        use serde::ser::SerializeStruct;
192        let mut s = serializer.serialize_struct("TaskId", 3)?;
193        s.serialize_field("language", self.language())?;
194        s.serialize_field("module", self.module())?;
195        s.serialize_field("name", self.name())?;
196        s.end()
197    }
198}
199
200impl<'de> Deserialize<'de> for TaskId {
201    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
202        #[derive(Deserialize)]
203        struct Fields {
204            #[serde(default)]
205            language: String,
206            module: String,
207            name: String,
208        }
209        let f = Fields::deserialize(deserializer)?;
210        Ok(TaskId::from_parts(&f.language, &f.module, &f.name))
211    }
212}
213
214impl fmt::Display for TaskId {
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        if self.lang_end == 0 {
217            write!(f, "{}.{}", self.module(), self.name())
218        } else {
219            write!(f, "{}::{}.{}", self.language(), self.module(), self.name())
220        }
221    }
222}
223
224/// Error returned when parsing a `TaskId` from a string fails.
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct ParseTaskIdError(String);
227
228impl fmt::Display for ParseTaskIdError {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        f.write_str(&self.0)
231    }
232}
233
234impl std::error::Error for ParseTaskIdError {}
235
236impl FromStr for TaskId {
237    type Err = ParseTaskIdError;
238
239    /// Parse a `TaskId` from its canonical string representation.
240    ///
241    /// Accepted formats:
242    /// - `module.name` — local task
243    /// - `language::module.name` — foreign task
244    ///
245    /// Returns an error if the string is empty, contains no `.` separator
246    /// (in the module.name portion), or has empty module or name components.
247    fn from_str(s: &str) -> Result<Self, Self::Err> {
248        if s.is_empty() {
249            return Err(ParseTaskIdError("empty task ID string".to_owned()));
250        }
251
252        let (language, rest) = if let Some(lang_end) = s.find("::") {
253            let lang = &s[..lang_end];
254            if lang.is_empty() {
255                return Err(ParseTaskIdError("empty language before '::'".to_owned()));
256            }
257            (lang, &s[lang_end + 2..])
258        } else {
259            ("", s)
260        };
261
262        let dot_pos = rest
263            .rfind('.')
264            .ok_or_else(|| ParseTaskIdError(format!("no '.' separator in task ID: {:?}", s)))?;
265
266        let module = &rest[..dot_pos];
267        let name = &rest[dot_pos + 1..];
268
269        if module.is_empty() {
270            return Err(ParseTaskIdError(format!(
271                "empty module in task ID: {:?}",
272                s
273            )));
274        }
275        if name.is_empty() {
276            return Err(ParseTaskIdError(format!("empty name in task ID: {:?}", s)));
277        }
278
279        if language.is_empty() {
280            Ok(TaskId::new(module, name))
281        } else {
282            Ok(TaskId::foreign(language, module, name))
283        }
284    }
285}
286
287/// Uniquely identifies a specific call (task + serialized arguments).
288///
289/// The `args_id` is a SHA-256 hash of the sorted serialized arguments,
290/// making it deterministic for the same inputs.
291#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
292pub struct CallId {
293    pub task_id: TaskId,
294    pub args_id: Arc<str>,
295}
296
297impl CallId {
298    pub fn new(task_id: TaskId, args_id: impl Into<Arc<str>>) -> Self {
299        let args_id: Arc<str> = args_id.into();
300        debug_assert!(
301            !args_id.contains(':'),
302            "CallId args_id must not contain ':' (breaks Display/FromStr round-trip): {args_id:?}"
303        );
304        Self { task_id, args_id }
305    }
306}
307
308impl fmt::Display for CallId {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        write!(f, "{}:{}", self.task_id, self.args_id)
311    }
312}
313
314/// Error returned when parsing a `CallId` from a string fails.
315#[derive(Debug, Clone, PartialEq, Eq)]
316#[non_exhaustive]
317pub enum ParseCallIdError {
318    /// A formatting / structure issue with the call ID string itself.
319    Format(String),
320    /// The embedded task ID portion could not be parsed.
321    TaskId(ParseTaskIdError),
322}
323
324impl fmt::Display for ParseCallIdError {
325    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
326        match self {
327            Self::Format(msg) => f.write_str(msg),
328            Self::TaskId(e) => write!(f, "invalid task_id in call ID: {e}"),
329        }
330    }
331}
332
333impl std::error::Error for ParseCallIdError {
334    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
335        match self {
336            Self::TaskId(e) => Some(e),
337            Self::Format(_) => None,
338        }
339    }
340}
341
342impl FromStr for CallId {
343    type Err = ParseCallIdError;
344
345    /// Parse a `CallId` from `task_id:args_id` format.
346    fn from_str(s: &str) -> Result<Self, Self::Err> {
347        // Find the last ':' that separates task_id from args_id
348        let colon_pos = s.rfind(':').ok_or_else(|| {
349            ParseCallIdError::Format(format!("no ':' separator in call ID: {:?}", s))
350        })?;
351        let task_str = &s[..colon_pos];
352        let args_id = &s[colon_pos + 1..];
353        if args_id.is_empty() {
354            return Err(ParseCallIdError::Format(format!(
355                "empty args_id in call ID: {:?}",
356                s
357            )));
358        }
359        let task_id = task_str
360            .parse::<TaskId>()
361            .map_err(ParseCallIdError::TaskId)?;
362        Ok(CallId::new(task_id, args_id))
363    }
364}
365
366/// Uniquely identifies a single invocation instance (UUID).
367///
368/// Each time a call is routed for execution, a new invocation is created.
369/// Multiple invocations can exist for the same call (retries, concurrent runs).
370#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
371pub struct InvocationId(Arc<str>);
372
373impl InvocationId {
374    pub fn new() -> Self {
375        Self(Arc::from(uuid::Uuid::new_v4().to_string()))
376    }
377
378    pub fn from_string(id: impl Into<Arc<str>>) -> Self {
379        Self(id.into())
380    }
381
382    /// Create an InvocationId from a string, validating UUID format.
383    pub fn try_from_string(id: impl Into<Arc<str>>) -> Result<Self, String> {
384        let s: Arc<str> = id.into();
385        uuid::Uuid::parse_str(&s).map_err(|e| format!("invalid UUID for InvocationId: {e}"))?;
386        Ok(Self(s))
387    }
388
389    /// Get the inner string value.
390    pub fn as_str(&self) -> &str {
391        &self.0
392    }
393}
394
395impl Default for InvocationId {
396    fn default() -> Self {
397        Self::new()
398    }
399}
400
401impl fmt::Display for InvocationId {
402    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403        write!(f, "{}", self.0)
404    }
405}
406
407impl AsRef<str> for InvocationId {
408    fn as_ref(&self) -> &str {
409        &self.0
410    }
411}
412
413impl From<String> for InvocationId {
414    fn from(s: String) -> Self {
415        Self(Arc::from(s))
416    }
417}
418
419impl From<&str> for InvocationId {
420    fn from(s: &str) -> Self {
421        Self(Arc::from(s))
422    }
423}
424
425/// Identifies a runner instance processing invocations.
426#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
427pub struct RunnerId(Arc<str>);
428
429impl RunnerId {
430    pub fn new() -> Self {
431        Self(Arc::from(uuid::Uuid::new_v4().to_string()))
432    }
433
434    pub fn from_string(id: impl Into<Arc<str>>) -> Self {
435        Self(id.into())
436    }
437
438    /// Create a RunnerId from a string, validating UUID format.
439    pub fn try_from_string(id: impl Into<Arc<str>>) -> Result<Self, String> {
440        let s: Arc<str> = id.into();
441        uuid::Uuid::parse_str(&s).map_err(|e| format!("invalid UUID for RunnerId: {e}"))?;
442        Ok(Self(s))
443    }
444
445    /// Get the inner string value.
446    pub fn as_str(&self) -> &str {
447        &self.0
448    }
449}
450
451impl Default for RunnerId {
452    fn default() -> Self {
453        Self::new()
454    }
455}
456
457impl fmt::Display for RunnerId {
458    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459        write!(f, "{}", self.0)
460    }
461}
462
463impl AsRef<str> for RunnerId {
464    fn as_ref(&self) -> &str {
465        &self.0
466    }
467}
468
469impl From<String> for RunnerId {
470    fn from(s: String) -> Self {
471        Self(Arc::from(s))
472    }
473}
474
475impl From<&str> for RunnerId {
476    fn from(s: &str) -> Self {
477        Self(Arc::from(s))
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484
485    #[test]
486    fn task_id_display() {
487        let tid = TaskId::new("myapp.tasks", "process_order");
488        assert_eq!(tid.to_string(), "myapp.tasks.process_order");
489    }
490
491    #[test]
492    fn task_id_foreign_display() {
493        let tid = TaskId::foreign("python", "analytics.tasks", "train_model");
494        assert_eq!(tid.to_string(), "python::analytics.tasks.train_model");
495    }
496
497    #[test]
498    fn task_id_is_foreign() {
499        let local = TaskId::new("mod", "func");
500        assert!(!local.is_foreign());
501        assert_eq!(local.language(), "");
502
503        let foreign = TaskId::foreign("rust", "mod", "func");
504        assert!(foreign.is_foreign());
505        assert_eq!(foreign.language(), "rust");
506    }
507
508    #[test]
509    fn task_id_local_and_foreign_not_equal() {
510        let local = TaskId::new("mod", "func");
511        let foreign = TaskId::foreign("rust", "mod", "func");
512        assert_ne!(local, foreign);
513    }
514
515    #[test]
516    fn call_id_display() {
517        let cid = CallId::new(TaskId::new("myapp.tasks", "process_order"), "abc123");
518        assert_eq!(cid.to_string(), "myapp.tasks.process_order:abc123");
519    }
520
521    #[test]
522    fn invocation_id_uniqueness() {
523        let id1 = InvocationId::new();
524        let id2 = InvocationId::new();
525        assert_ne!(id1, id2);
526    }
527
528    #[test]
529    fn invocation_id_from_string() {
530        let id = InvocationId::from_string("my-custom-id");
531        assert_eq!(&*id.0, "my-custom-id");
532        assert_eq!(id.to_string(), "my-custom-id");
533    }
534
535    #[test]
536    fn invocation_id_default() {
537        let id = InvocationId::default();
538        assert!(!id.as_str().is_empty());
539    }
540
541    #[test]
542    fn runner_id_basics() {
543        let r1 = RunnerId::new();
544        let r2 = RunnerId::new();
545        assert_ne!(r1, r2);
546        assert!(!r1.to_string().is_empty());
547    }
548
549    #[test]
550    fn runner_id_from_string() {
551        let r = RunnerId::from_string("worker-1");
552        assert_eq!(&*r.0, "worker-1");
553        assert_eq!(r.to_string(), "worker-1");
554    }
555
556    #[test]
557    fn runner_id_default() {
558        let r = RunnerId::default();
559        assert!(!r.as_str().is_empty());
560    }
561
562    #[test]
563    fn serde_round_trip_task_id() {
564        let tid = TaskId::new("myapp", "process");
565        let json = serde_json::to_string(&tid).unwrap();
566        let back: TaskId = serde_json::from_str(&json).unwrap();
567        assert_eq!(tid, back);
568    }
569
570    #[test]
571    fn serde_round_trip_invocation_id() {
572        let id = InvocationId::from_string("abc-123");
573        let json = serde_json::to_string(&id).unwrap();
574        let back: InvocationId = serde_json::from_str(&json).unwrap();
575        assert_eq!(id, back);
576    }
577
578    #[test]
579    fn serde_round_trip_call_id() {
580        let cid = CallId::new(TaskId::new("m", "f"), "hash123");
581        let json = serde_json::to_string(&cid).unwrap();
582        let back: CallId = serde_json::from_str(&json).unwrap();
583        assert_eq!(cid, back);
584    }
585
586    #[test]
587    fn task_id_accessors() {
588        let tid = TaskId::new("myapp.tasks", "process_order");
589        assert_eq!(tid.module(), "myapp.tasks");
590        assert_eq!(tid.name(), "process_order");
591    }
592
593    #[test]
594    fn serde_backward_compat_missing_language() {
595        // Old JSON without language field should deserialize with empty language
596        let json = r#"{"module":"myapp","name":"process"}"#;
597        let tid: TaskId = serde_json::from_str(json).unwrap();
598        assert_eq!(tid.language(), "");
599        assert_eq!(tid.module(), "myapp");
600        assert_eq!(tid.name(), "process");
601        assert!(!tid.is_foreign());
602    }
603
604    #[test]
605    fn serde_round_trip_foreign_task_id() {
606        let tid = TaskId::foreign("python", "analytics.tasks", "train_model");
607        let json = serde_json::to_string(&tid).unwrap();
608        let back: TaskId = serde_json::from_str(&json).unwrap();
609        assert_eq!(tid, back);
610        assert!(back.is_foreign());
611        assert_eq!(back.language(), "python");
612    }
613
614    #[test]
615    fn invocation_id_as_str() {
616        let id = InvocationId::from_string("test-id");
617        assert_eq!(id.as_str(), "test-id");
618    }
619
620    #[test]
621    fn invocation_id_try_from_string_valid() {
622        let uuid_str = uuid::Uuid::new_v4().to_string();
623        let result = InvocationId::try_from_string(uuid_str.clone());
624        assert!(result.is_ok());
625        assert_eq!(result.unwrap().as_str(), uuid_str);
626    }
627
628    #[test]
629    fn invocation_id_try_from_string_invalid() {
630        let result = InvocationId::try_from_string("not-a-uuid");
631        assert!(result.is_err());
632        assert!(result.unwrap_err().contains("invalid UUID"));
633    }
634
635    #[test]
636    fn runner_id_as_str() {
637        let r = RunnerId::from_string("worker-1");
638        assert_eq!(r.as_str(), "worker-1");
639    }
640
641    // --- FromStr / parse_task_id tests ---
642
643    #[test]
644    fn parse_local_task_id() {
645        let tid: TaskId = "myapp.tasks.process_order".parse().unwrap();
646        assert_eq!(tid.module(), "myapp.tasks");
647        assert_eq!(tid.name(), "process_order");
648        assert!(!tid.is_foreign());
649    }
650
651    #[test]
652    fn parse_foreign_task_id() {
653        let tid: TaskId = "python::analytics.tasks.train_model".parse().unwrap();
654        assert_eq!(tid.language(), "python");
655        assert_eq!(tid.module(), "analytics.tasks");
656        assert_eq!(tid.name(), "train_model");
657        assert!(tid.is_foreign());
658    }
659
660    #[test]
661    fn parse_simple_task_id() {
662        let tid: TaskId = "mod.func".parse().unwrap();
663        assert_eq!(tid.module(), "mod");
664        assert_eq!(tid.name(), "func");
665    }
666
667    #[test]
668    fn parse_empty_string_fails() {
669        let result = "".parse::<TaskId>();
670        assert!(result.is_err());
671        assert!(result.unwrap_err().to_string().contains("empty"));
672    }
673
674    #[test]
675    fn parse_no_dot_fails() {
676        let result = "nodot".parse::<TaskId>();
677        assert!(result.is_err());
678        assert!(result.unwrap_err().to_string().contains("no '.'"));
679    }
680
681    #[test]
682    fn parse_trailing_dot_fails() {
683        let result = "module.".parse::<TaskId>();
684        assert!(result.is_err());
685        assert!(result.unwrap_err().to_string().contains("empty name"));
686    }
687
688    #[test]
689    fn parse_leading_dot_fails() {
690        let result = ".name".parse::<TaskId>();
691        assert!(result.is_err());
692        assert!(result.unwrap_err().to_string().contains("empty module"));
693    }
694
695    #[test]
696    fn parse_empty_language_fails() {
697        let result = "::mod.func".parse::<TaskId>();
698        assert!(result.is_err());
699        assert!(result.unwrap_err().to_string().contains("empty language"));
700    }
701
702    #[test]
703    fn parse_foreign_no_dot_fails() {
704        let result = "rust::nodot".parse::<TaskId>();
705        assert!(result.is_err());
706        assert!(result.unwrap_err().to_string().contains("no '.'"));
707    }
708
709    #[test]
710    fn display_parse_round_trip_local() {
711        let original = TaskId::new("myapp.tasks", "process_order");
712        let serialized = original.to_string();
713        let parsed: TaskId = serialized.parse().unwrap();
714        assert_eq!(original, parsed);
715    }
716
717    #[test]
718    fn display_parse_round_trip_foreign() {
719        let original = TaskId::foreign("python", "analytics.tasks", "train_model");
720        let serialized = original.to_string();
721        let parsed: TaskId = serialized.parse().unwrap();
722        assert_eq!(original, parsed);
723    }
724
725    #[test]
726    fn display_parse_round_trip_simple() {
727        let original = TaskId::new("mod", "func");
728        let serialized = original.to_string();
729        let parsed: TaskId = serialized.parse().unwrap();
730        assert_eq!(original, parsed);
731    }
732
733    #[test]
734    fn display_parse_round_trip_dotted_module() {
735        let original = TaskId::new("a.b.c", "func");
736        let serialized = original.to_string();
737        let parsed: TaskId = serialized.parse().unwrap();
738        assert_eq!(original, parsed);
739    }
740}