Parse a single ILP line. Format: `measurement[,tag=val]* field=val[,field=val]* [timestamp]`
(line: &str)
| 72 | /// |
| 73 | /// Format: `measurement[,tag=val]* field=val[,field=val]* [timestamp]` |
| 74 | pub fn parse_line(line: &str) -> Result<IlpLine<'_>, IlpError> { |
| 75 | let line = line.trim(); |
| 76 | if line.is_empty() || line.starts_with('#') { |
| 77 | return Err(IlpError::EmptyLine); |
| 78 | } |
| 79 | |
| 80 | // Split into: measurement+tags, fields, optional timestamp. |
| 81 | // First space separates measurement+tags from fields. |
| 82 | let first_space = line.find(' ').ok_or(IlpError::MissingFields)?; |
| 83 | let measurement_tags = &line[..first_space]; |
| 84 | let rest = line[first_space + 1..].trim_start(); |
| 85 | |
| 86 | // Parse measurement and tags. |
| 87 | let (measurement, tags) = parse_measurement_tags(measurement_tags)?; |
| 88 | |
| 89 | // Split fields from optional timestamp (last space). |
| 90 | let (fields_str, timestamp_ns) = if let Some(last_space) = rest.rfind(' ') { |
| 91 | let maybe_ts = &rest[last_space + 1..]; |
| 92 | match maybe_ts.parse::<i64>() { |
| 93 | Ok(ts) => (&rest[..last_space], Some(ts)), |
| 94 | Err(_) => (rest, None), // Not a valid timestamp — treat all as fields. |
| 95 | } |
| 96 | } else { |
| 97 | (rest, None) |
| 98 | }; |
| 99 | |
| 100 | let fields = parse_fields(fields_str)?; |
| 101 | if fields.is_empty() { |
| 102 | return Err(IlpError::MissingFields); |
| 103 | } |
| 104 | |
| 105 | Ok(IlpLine { |
| 106 | measurement, |
| 107 | tags, |
| 108 | fields, |
| 109 | timestamp_ns, |
| 110 | }) |
| 111 | } |
| 112 | |
| 113 | /// Parse a batch of ILP lines. Skips empty lines and comments. |
| 114 | pub fn parse_batch(input: &str) -> Vec<Result<IlpLine<'_>, IlpError>> { |