Plantuml encoding in Rust using TDD
source link: https://maksugr.com/posts/plantuml-encoding-in-rust-using-tdd
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Plantuml encoding in Rust using TDDmaksugr
Plantuml encoding in Rust using TDD
Sum up the experience of building plantuml encoding crate in Rust using TDD. Could be helpful for those interested in plantuml, newcomers to Rust, and everyone who is concerned with creating crate from scratch.
Plantuml is a little bit old-fashed but a great tool to create diagrams as code. It needs Java to process logic and Graphviz for drawing. If you don’t want to install all this stuff, you can use plantuml text encoding mechanism. It offers you a way to encode your plantuml code either to something like base64 with deflate compression or to hex and send the result to the plantuml server via URL for rendering.
For example, you can encode with deflate compression
@startuml
PUML -> RUST
@enduml
to SoWkIImgAStDuGe8zVLHqBLJ20eD3k5oICrB0Ge20000
and use it as a part of the URL to the plantuml server — quite a long link to the plantuml server.
There is a library for encoding/decoding plantuml in Rust — plantuml_encoding. In this article, we will go through the development of that library from scratch with minimal omissions. We will meet library crate creation, TDD development approach, AsRef
, error handling, documentation, and of cause plantuml.
First of all, let’s create a library.
cargo new plantuml_encoding --lib
cd plantuml_encoding
We can check that everything is ok and base test in lib.rs
passes.
cargo test
As we decided to use TDD in the project it’s a good idea to start writing tests. Our TDD plan is the same as described in The Book:
- Write a test that fails and run it to make sure it fails for the reason you expect.
- Write or modify just enough code to make the new test pass.
- Refactor the code you just added or changed and make sure the tests continue to pass.
- Repeat from step 1!
Let’s remove boilerplate code from lib.rs
and add the first test.
#[cfg(test)]
mod tests {
#[test]
fn it_encodes_plantuml_hex() {
assert_eq!(
encode_plantuml_hex("@startuml\nPUML -> RUST: HELLO \n@enduml"),
""
);
}
}
We created module tests
and assert that the result of the execution of the function encode_plantuml_hex
will equal some string. Run cargo test
and we will fail with the reason we expected:
error[E0425]: cannot find function `encode_plantuml_hex` in this scope
--> src/lib.rs:6:13
|
6 | encode_plantuml_hex("@startuml\nPUML -> RUST: HELLO \n@enduml"),
| ^^^^^^^^^^^^^^^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
error: could not compile `plantuml_encoding` due to previous error
Time to add encode_plantuml_hex
! We can do it in the same file just over our tests
module:
pub fn encode_plantuml_hex(plantuml: &str) -> String {
String::from("")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_encodes_plantuml_hex() {
assert_eq!(
encode_plantuml_hex("@startuml\nPUML -> RUST: HELLO \n@enduml"),
""
);
}
}
encode_plantuml_hex
takes &str
and returns String
. Notice we added use super::*;
to tests
module to let it see encode_plantuml_hex
. cargo test
and all (just one for now) tests are passed! But now we need to meet our target logic and encode plantuml to hex (not just return an empty string slice from the function).
To do that we need a general encoder from string to hex. Not a joke but in Rust we have one — hex. To add it to the project we can just add it to the Cargo.toml
:
[dependencies]
hex = "0.4"
And now we can use hex
in encode_plantuml_hex
:
pub fn encode_plantuml_hex(plantuml: &str) -> String {
let hex = hex::encode(plantuml);
String::from("~h") + &hex
}
We use encode
method of hex
and then add to the result ~h
to meet the plantuml server requirements for hex. cargo test
and… we fail. But it’s great!
running 1 test
test tests::it_encodes_plantuml_hex ... FAILED
failures:
---- tests::it_encodes_plantuml_hex stdout ----
thread 'tests::it_encodes_plantuml_hex' panicked at 'assertion failed: `(left == right)`
left: `"~h407374617274756d6c0a50554d4c202d3e20525553543a2048454c4c4f200a40656e64756d6c"`,
right: `""`', src/lib.rs:13:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_encodes_plantuml_hex
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
We got what we wanted. left
and right
parts are no longer equal and we get actual hex in the left
part. Let’s check this hex on the plantuml server. It works!
So we can update the test to target:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_encodes_plantuml_hex() {
assert_eq!(
encode_plantuml_hex("@startuml\nPUML -> RUST: HELLO \n@enduml"),
"~h407374617274756d6c0a50554d4c202d3e20525553543a2048454c4c4f200a40656e64756d6c"
);
}
}
And it won’t fail again. We have our first plantuml encoding function!
We start the decoding part from the same starting point — the test. Let’s add it_decodes_plantuml_hex
test to tests
module.
#[test]
fn it_decodes_plantuml_hex() {
assert_eq!(
decode_plantuml_hex("~h407374617274756d6c0a50554d4c202d3e20525553543a2048454c4c4f200a40656e64756d6c"),
"@startuml\nPUML -> RUST: HELLO \n@enduml"
);
}
For this time we already know plantuml and hex string. cargo test
doesn’t let us forget to write the target function.
error[E0425]: cannot find function `decode_plantuml_hex` in this scope
--> src/lib.rs:22:13
|
1 | fn encode_plantuml_hex(plantuml: &str) -> String {
| ------------------------------------------------ similarly named function `encode_plantuml_hex` defined here
...
22 | decode_plantuml_hex("~h407374617274756d6c0a50554d4c202d3e20525553543a2048454c4c4f200a40656e64756d6c"),
| ^^^^^^^^^^^^^^^^^^^ help: a function with a similar name exists: `encode_plantuml_hex`
And we don’t mind. decode_plantuml_hex
is a little bit complex then encode_plantuml_hex
:
pub fn decode_plantuml_hex(hex: &str) -> String {
let plantuml_hex_trimmed = hex.trim_start_matches("~h");
let decoded_bytes = hex::decode(plantuml_hex_trimmed).unwrap();
String::from_utf8(decoded_bytes).unwrap()
}
That time we need to use unwrap
because hex::decode
and String::from_utf8
can produce errors. For now, it ok to just unwrap
, we will properly handle errors later.
In decode_plantuml_hex
we trimmed ~h
from the beginning (actually it is not part of the hex), decode it to bytes and get a string from the bytes.
cargo test
looks happy, everything is fine!
AsRef
encode_plantuml_hex
and decode_plantuml_hex
functions take &str
as an arguments. It’s ok but it may be inconvenient if you have, for example, String
. The compiler won’t allow you use String
as an argument. Let’s write test it_encodes_plantuml_hex_from_string
to check it:
#[test]
fn it_encodes_plantuml_hex_from_string() {
assert_eq!(
encode_plantuml_hex("@startuml\nPUML -> RUST: HELLO \n@enduml".to_string()),
"~h407374617274756d6c0a50554d4c202d3e20525553543a2048454c4c4f200a40656e64756d6c"
);
}
Notice call to_string
(to create String
from &str
) for the argument of encode_plantuml_hex
. And there is an error:
error[E0308]: mismatched types
--> src/lib.rs:30:33
|
30 | encode_plantuml_hex("@startuml\nPUML -> RUST: HELLO \n@enduml".to_string()),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| |
| expected `&str`, found struct `String`
| help: consider borrowing here: `&"@startuml\nPUML -> RUST: HELLO \n@enduml".to_string()`
To fix that we can use AsRef train to add type conversion and convert functions to generic functions:
pub fn encode_plantuml_hex<T: AsRef<str>>(plantuml: T) -> String {
let hex = hex::encode(plantuml.as_ref());
String::from("~h") + &hex
}
pub fn decode_plantuml_hex<T: AsRef<str>>(hex: T) -> String {
let plantuml_hex_trimmed = hex.as_ref().trim_start_matches("~h");
let decoded_bytes = hex::decode(plantuml_hex_trimmed).unwrap();
String::from_utf8(decoded_bytes).unwrap()
}
For example: By creating a generic function that takes an
AsRef<str>
we express that we want to accept all references that can be converted to &str as an argument. Since bothString
and&str
implementAsRef<str>
we can accept both as input argument.
Nothing more, nothing less to the documentation! But notice that we not only change the signature of the functions but also add as_ref
call to params to make a conversion in place.
Now it_encodes_plantuml_hex_from_string
test is passed and we can use either &str
or String
as arguments for the functions.
Error handling
unwrap
is good during prototyping but should be replaced by an error handling mechanism in the stable version of the library.
decode_plantuml_hex
function can produce errors. If we remove unwrap
the compiler will say to us:
error[E0308]: mismatched types
--> src/lib.rs:16:23
|
16 | String::from_utf8(decoded_bytes)
| ^^^^^^^^^^^^^ expected struct `Vec`, found enum `Result`
|
= note: expected struct `Vec<_>`
found enum `Result<Vec<_>, FromHexError>`
error[E0308]: mismatched types
--> src/lib.rs:16:5
|
11 | pub fn decode_plantuml_hex<T: AsRef<str>>(hex: T) -> String {
| ------ expected `String` because of return type
...
16 | String::from_utf8(decoded_bytes)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `String`, found enum `Result`
|
= note: expected struct `String`
found enum `Result<String, FromUtf8Error>`
There are two mismatched types
errors: from hex::decode
(without unwrap
it returns Result<Vec<_>, FromHexError>
because we didn’t unwrap it) and from String::from_utf8
(without unwrap
it returns Result<String, FromUtf8Error>
). So we have two different types of errors but should have only one.
We can fix it by starting to provide to the users of the library Result<String, FromPlantumlError>
. FromPlantumlError
— error type of the library that encapsulates these types of errors.
But, as always, let’s start with a test:
#[test]
fn it_decode_plantuml_hex_error() {
assert_eq!(
decode_plantuml_hex("12345"),
Err(errors::FromPlantumlError(
"there is a problem during hex decoding: `Odd number of digits`".to_string()
))
);
}
Let’s create file errors.rs
and define struct as the error type of the library:
#[derive(Debug, PartialEq)]
pub struct FromPlantumlError (pub String);
As you probably noticed we’ve also added #[derive(Debug, PartialEq)]
: Debug
for display and PartialEq
for tests. We can import FromPlantumlError
by adding these lines at the beginning of the lib.rs
:
mod errors;
pub use crate::errors::FromPlantumlError;
And let’s add Result<String, FromPlantumlError>
to decode_plantuml_hex
and try to fix it:
pub fn decode_plantuml_hex<T: AsRef<str>>(hex: T) -> Result<String, FromPlantumlError> {
let plantuml_hex_trimmed = hex.as_ref().trim_start_matches("~h");
let decoded_bytes = hex::decode(plantuml_hex_trimmed)?;
Ok(String::from_utf8(decoded_bytes)?)
}
We replaced unwrap
with ? to propagate error forward but with conversion to our type of error — FromPlantumlError
. Also, we need to fix one of the tests (wrap in to Ok()
as the result of the decode_plantuml_hex
):
#[test]
fn it_decodes_plantuml_hex() {
assert_eq!(
decode_plantuml_hex("~h407374617274756d6c0a50554d4c202d3e20525553543a2048454c4c4f200a40656e64756d6c"),
Ok("@startuml\nPUML -> RUST: HELLO \n@enduml".to_string())
);
}
But it seems we still have the compile-time problems (in Rust it is common practice and ok, duh):
error[E0277]: `?` couldn't convert the error to `FromPlantumlError`
--> src/lib.rs:14:58
|
11 | pub fn decode_plantuml_hex<T: AsRef<str>>(hex: T) -> Result<String, FromPlantumlError> {
| ----------------------------- expected `FromPlantumlError` because of this
...
14 | let decoded_bytes = hex::decode(plantuml_hex_trimmed)?;
| ^ the trait `From<FromHexError>` is not implemented for `FromPlantumlError`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= note: required because of the requirements on the impl of `FromResidual<Result<Infallible, FromHexError>>` for `Result<String, FromPlantumlError>`
error[E0277]: `?` couldn't convert the error to `FromPlantumlError`
--> src/lib.rs:16:40
|
11 | pub fn decode_plantuml_hex<T: AsRef<str>>(hex: T) -> Result<String, FromPlantumlError> {
| ----------------------------- expected `FromPlantumlError` because of this
...
16 | Ok(String::from_utf8(decoded_bytes)?)
| ^ the trait `From<FromUtf8Error>` is not implemented for `FromPlantumlError`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= note: required because of the requirements on the impl of `FromResidual<Result<Infallible, FromUtf8Error>>` for `Result<String, FromPlantumlError>`
In short, the compiler can’t convert FromHexError
and FromUtf8Error
to FromPlantumlError
and advices implement traits From<FromHexError>
and From<FromUtf8Error>
(trait From) for FromPlantumlError
. So, let’s do it in our errors.rs
file!
use std::{convert, string};
#[derive(Debug, PartialEq)]
pub struct FromPlantumlError (pub String);
impl convert::From<string::FromUtf8Error> for FromPlantumlError {
fn from(err: string::FromUtf8Error) -> Self {
FromPlantumlError(format!(
"there is a problem during decoding: `{}`",
err
))
}
}
impl convert::From<hex::FromHexError> for FromPlantumlError {
fn from(err: hex::FromHexError) -> Self {
FromPlantumlError(format!("there is a problem during hex decoding: `{}`", err))
}
}
We told the compiler how to convert one type to another and cargo test
is happy once again — it compiles and tests are succeed.
running 4 tests
test tests::it_encodes_plantuml_hex_from_string ... ok
test tests::it_encodes_plantuml_hex ... ok
test tests::it_decode_plantuml_hex_error ... ok
test tests::it_decodes_plantuml_hex ... ok
encode_plantuml_hex
function can’t produce errors like decode_plantuml_hex
. Although it’s a matter of time before internal changes of encode_plantuml_hex
will require to handle errors. So for consistency and backward compatibility, it is handy to add error handling to encode_plantuml_hex
too.
pub fn encode_plantuml_hex<T: AsRef<str>>(plantuml: T) -> Result<String, FromPlantumlError> {
let hex = hex::encode(plantuml.as_ref());
Ok(String::from("~h") + &hex)
}
And we need to make Ok()
change for encode_plantuml_hex
in tests:
#[test]
fn it_encodes_plantuml_hex() {
assert_eq!(
encode_plantuml_hex("@startuml\nPUML -> RUST: HELLO \n@enduml"),
Ok("~h407374617274756d6c0a50554d4c202d3e20525553543a2048454c4c4f200a40656e64756d6c".to_string())
);
}
#[test]
fn it_encodes_plantuml_hex_from_string() {
assert_eq!(
encode_plantuml_hex("@startuml\nPUML -> RUST: HELLO \n@enduml".to_string()),
Ok("~h407374617274756d6c0a50554d4c202d3e20525553543a2048454c4c4f200a40656e64756d6c".to_string())
);
}
Deflate
It seems that we finished with the hex part. Hence we are ready to start with the deflate part. As always we start from a test:
#[test]
fn it_encodes_plantuml_deflate() {
assert_eq!(
encode_plantuml_deflate("@startuml\nPUML -> RUST: HELLO \n@enduml"),
Ok("".to_string())
);
}
And immediate add encode_plantuml_deflate
implementation:
pub fn encode_plantuml_deflate<T: AsRef<str>>(
plantuml: T,
) -> Result<String, errors::FromPlantumlError> {
let mut encoder = write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
encoder.write_all(plantuml.as_ref().as_bytes())?;
let encoded_bytes = encoder.finish()?;
Ok(utils::encode_plantuml_for_deflate(&encoded_bytes))
}
That a quite big a piece of code. Let’s move forward line by line. First of all, we define encode_plantuml_deflate
function in the same way that we did with encode_plantuml_hex
function. Then we create DeflateEncoder
encoder from flate2 crate. It helps us to make deflate compression.
[dependencies]
hex = "0.4"
flate2 = "1.0.24"
And at the top of the lib.rs
:
use std::io::prelude::*;
use flate2::write;
DeflateEncoder
will get all bytes of the plantuml string and compress. But unlike the hex encoding for delfate encoding the plantuml server needs not just a compressed version of the plantuml string it needs a special encoding string. That what encode_plantuml_for_deflate
do. We won’t dive into details because it doesn’t matter at all. All we need to import that function from the utils
module on the top of the lib.rs
file:
mod utils;
And content for the utils.rs
file I’ll just give you as is (javascript → rust version). You need to create the file and add this code:
fn encode_6_bit(mut b: u8) -> String {
if b < 10 {
return String::from((48 + b) as char);
}
b -= 10;
if b < 26 {
return String::from((65 + b) as char);
}
b -= 26;
if b < 26 {
return String::from((97 + b) as char);
}
b -= 26;
if b == 0 {
return String::from("-");
}
if b == 1 {
return String::from("_");
}
String::from("?")
}
fn append_3_bytes(b1: &u8, b2: &u8, b3: &u8) -> String {
let c1 = b1 >> 2;
let c2 = ((b1 & 0x3) << 4) | (b2 >> 4);
let c3 = ((b2 & 0xF) << 2) | (b3 >> 6);
let c4 = b3 & 0x3F;
let mut result = String::new();
result += &encode_6_bit(c1 & 0x3F);
result += &encode_6_bit(c2 & 0x3F);
result += &encode_6_bit(c3 & 0x3F);
result += &encode_6_bit(c4 & 0x3F);
result
}
pub fn encode_plantuml_for_deflate(encoded_bytes: &[u8]) -> String {
let mut result = String::new();
for (index, byte) in encoded_bytes.iter().enumerate().step_by(3) {
if index + 2 == encoded_bytes.len() {
result += &append_3_bytes(byte, &encoded_bytes[index + 1], &0);
continue;
}
if index + 1 == encoded_bytes.len() {
result += &append_3_bytes(byte, &0, &0);
continue;
}
result += &append_3_bytes(byte, &encoded_bytes[index + 1], &encoded_bytes[index + 2]);
}
result
}
Let’s try to run cargo test
. There are two issues:
error[E0277]: `?` couldn't convert the error to `FromPlantumlError`
--> src/lib.rs:26:52
|
24 | ) -> Result<String, errors::FromPlantumlError> {
| ----------------------------------------- expected `FromPlantumlError` because of this
25 | let mut encoder = write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
26 | encoder.write_all(plantuml.as_ref().as_bytes())?;
| ^ the trait `From<std::io::Error>` is not implemented for `FromPlantumlError`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= help: the following implementations were found:
<FromPlantumlError as From<FromHexError>>
<FromPlantumlError as From<FromUtf8Error>>
= note: required because of the requirements on the impl of `FromResidual<Result<Infallible, std::io::Error>>` for `Result<String, FromPlantumlError>`
error[E0277]: `?` couldn't convert the error to `FromPlantumlError`
--> src/lib.rs:28:41
|
24 | ) -> Result<String, errors::FromPlantumlError> {
| ----------------------------------------- expected `FromPlantumlError` because of this
...
28 | let encoded_bytes = encoder.finish()?;
| ^ the trait `From<std::io::Error>` is not implemented for `FromPlantumlError`
|
= note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
= help: the following implementations were found:
<FromPlantumlError as From<FromHexError>>
<FromPlantumlError as From<FromUtf8Error>>
= note: required because of the requirements on the impl of `FromResidual<Result<Infallible, std::io::Error>>` for `Result<String, FromPlantumlError>`
Both issues are about absent implementation of the trait From<std::io::Error>
for FromPlantumlError
. But we already know how to handle this kind of issue. Don’t we? We just need to add another implementation to the errors.rs
file:
use std::{convert, io, string};
impl convert::From<io::Error> for FromPlantumlError {
fn from(err: io::Error) -> Self {
FromPlantumlError(format!(
"there is a problem during deflate decoding: `{}`",
err
))
}
}
cargo test
and our result is:
failures:
---- tests::it_encodes_plantuml_deflate stdout ----
thread 'tests::it_encodes_plantuml_deflate' panicked at 'assertion failed: `(left == right)`
left: `Ok("0IO0sVz0StHXSdHrRMmAK5LDJ20jFY1ILLDKEY18HKnCJo0AG6LkP7LjR000")`,
right: `Ok("")`', src/lib.rs:73:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_encodes_plantuml_deflate
So, as we know everything is fine and we’ve just gotten value for comparison in our tests. Let’s change the empty string slice to the left
value:
#[test]
fn it_encodes_plantuml_deflate() {
assert_eq!(
encode_plantuml_deflate("@startuml\nPUML -> RUST: HELLO \n@enduml"),
Ok("0IO0sVz0StHXSdHrRMmAK5LDJ20jFY1ILLDKEY18HKnCJo0AG6LkP7LjR000".to_string())
);
}
All tests are passed. And the link to plantuml works too!
It’s time to move to the deflate decoding. Let’s add deflate decoding test:
#[test]
fn it_decodes_plantuml_deflate() {
assert_eq!(
decode_plantuml_deflate("0IO0sVz0StHXSdHrRMmAK5LDJ20jFY1ILLDKEY18HKnCJo0AG6LkP7LjR000"),
Ok("@startuml\nPUML -> RUST: HELLO \n@enduml".to_string())
);
}
Then code for the utils.rs
:
fn decode_6_bit(s: String) -> Option<u8> {
let c = s.chars().next()? as u8;
if s == "_" {
return Some(63);
};
if s == "-" {
return Some(62);
}
if c >= 97 {
return Some(c - 61);
}
if c >= 65 {
return Some(c - 55);
}
if c >= 48 {
return Some(c - 48);
}
Some(0)
}
fn extract_3_bytes(chars: &[char]) -> Option<[u8; 3]> {
let mut chars = chars.iter();
let c1 = decode_6_bit(String::from(*chars.next()?))?;
let c2 = decode_6_bit(String::from(*chars.next()?))?;
let c3 = decode_6_bit(String::from(*chars.next()?))?;
let c4 = decode_6_bit(String::from(*chars.next()?))?;
let b1 = c1 << 2 | (c2 >> 4) & 0x3F;
let b2 = (c2 << 4) & 0xF0 | (c3 >> 2) & 0xF;
let b3 = (c3 << 6) & 0xC0 | c4 & 0x3F;
Some([b1, b2, b3])
}
pub fn decode_plantuml_for_deflate(decoded_string: &str) -> Option<Vec<u8>> {
let mut result = vec![];
for chunk in decoded_string.chars().collect::<Vec<char>>().chunks(4) {
result.extend(extract_3_bytes(chunk)?);
}
Some(result)
}
And implementation of the decode_plantuml_deflate
:
pub fn decode_plantuml_deflate<T: AsRef<str>>(
plantuml_deflated: T,
) -> Result<String, errors::FromPlantumlError> {
let result = match utils::decode_plantuml_for_deflate(plantuml_deflated.as_ref()) {
Some(r) => r,
None => {
return Err(errors::FromPlantumlError(
"internal decoding error (out of bounds or similar)".to_string(),
));
}
};
let mut deflater = write::DeflateDecoder::new(Vec::new());
for item in result.into_iter() {
// write_all produces `failed to write whole buffer` issue with some data
deflater.write(&[item])?;
}
let decoded_bytes = deflater.finish()?;
Ok(String::from_utf8(decoded_bytes)?)
}
decode_plantuml_deflate
function makes a bunch of things:
- decodes a string from special plantuml encoding
- converts possible errors to
FromPlantumlError
- deflate decompresses
Vec<u8>
- creates new a string from bytes
If you run cargo test
every test should be passed.
One more thing. We forgot to test the error handling of the decode_plantuml_deflate
function. Let’s fix that:
#[test]
fn it_decode_plantuml_deflate_error() {
assert_eq!(
decode_plantuml_deflate("4444"),
Err(errors::FromPlantumlError(
"there is a problem during deflate decoding: `deflate decompression error`"
.to_string()
))
);
}
All functions of the library are ready!
Documentation
To be the best crate in the world our library just needs good documentation. A few last ideas for the lib.rs
:
//! Encoding and decoding text plantuml diagrams to facilitate communication of them through URL.
//!
//! ## Overview
//!
//! Consider the next plain text plantuml diagram:
//!
//! ```plantuml
//! @startuml
//! PUML -> RUST: HELLO
//! @enduml
//! ```
//!
//! It can be encoded to `0IO0sVz0StHXSdHrRMmAK5LDJ20jFY1ILLDKEY18HKnCJo0AG6LkP7LjR000` and with the help of the plantuml server (`https://www.plantuml.com/plantuml/uml/`) it can be shared [through URL](https://www.plantuml.com/plantuml/uml/0IO0sVz0StHXSdHrRMmAK5LDJ20jFY1ILLDKEY18HKnCJo0AG6LkP7LjR000).
//!
//! Also, it can be decoded in the opposite direction.
//!
comment is the documentation for the main file of the crate. And a little example of documenting one of the public functions:
/// Encode plantuml to hex
/// (with [additional prefix `~h`](https://plantuml.com/text-encoding))
///
/// ## Example
///
/// ```rust
/// use plantuml_encoding::{encode_plantuml_hex, FromPlantumlError};
///
/// fn main() -> Result<(), FromPlantumlError> {
/// let encoded_hex = encode_plantuml_hex("@startuml\nPUML -> RUST\n@enduml")?;
///
/// assert_eq!(encoded_hex, "~h407374617274756d6c0a50554d4c202d3e20525553540a40656e64756d6c");
///
/// Ok(())
/// }
/// ```
pub fn encode_plantuml_hex<T: AsRef<str>>(
plantuml: T,
) -> Result<String, errors::FromPlantumlError> {
let hex = hex::encode(plantuml.as_ref());
Ok(String::from("~h") + &hex)
}
///
comment for regular documentation note.
Don’t forget that code of the documentation is executable and runs on cargo test
. So that is not just not up-to-date examples. All examples in Rust are alive and it’s great. Run cargo test
and you will see that one doc test is passed:
Doc-tests plantuml_encoding
running 1 test
test src/lib.rs - encode_plantuml_hex (line 29) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.34s
That’s it!
We implemented a rust crate for encoding/decoding plantuml using TDD (twice!) from scratch and also on our way we discussed AsRef
, error handling, and documentation.
Thank you for your attention, will be glad to hear a word from you.
I'd be happy to hear your thoughts. Open an issue to discuss the post. Subscribe to be in touch 🖤
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK