- A Simple Web App in Rust, Part 2b
- Joel’s Journal
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: LeopPro
Developing a Simple Web application with Rust, Part 2b
directory
1 Series of articles
In this series of articles, I’ve documented my experience trying to develop a simple Web application using Rust.
So far, we have:
- Set goals & a “Hello World” level Web server
- Figure out how to write to a file
The last article was disgusting. This time we’ll explore Rust’s time and date formats, focusing on keeping time in a proper format.
2 use Chrono
A search for “date” in crates. IO yields a package called chrono. It’s hot and updated frequently, so it seems like a good candidate. From the README file, it has great date and time output.
The first thing is to add the Chrono dependency in Cargo. Toml, but before we do that, let’s move the old main.rs out to make room for experimentation:
$ ls
Cargo.lock Cargo.toml log.txt src target
$ cd src/
$ ls
main.rs web_main.rs
$ git mv main.rs main_file_writing.rs
$ touch main.rs
$ git add main.rs
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
modified: main.rs
copied: main.rs -> main_file_writing.rs
Untracked files:
(use "git add <file>..." to include inwhat will be committed) .. /log.txt $ git commit -m'move file writing out of the way for working with dates'
[master 4cd2b0e] move file writing out of the way for working with dates
2 files changed, 16 deletions(-)
rewrite src/main.rs (100%)
copy src/{main.rs => main_file_writing.rs} (100%)
Copy the code
Add Chrono dependency to Cargo. Toml:
[package]
name = "simple-log"
version = "0.1.0 from"
authors = ["Joel McCracken <[email protected]>"]
[dependencies]
chrono = "0.2"
[dependencies.nickel]
git = "https://github.com/nickel-org/nickel.rs.git"
Copy the code
The readme document continues:
And put this in your crate root:
extern crate chrono;
Copy the code
I don’t know what this means, but I’m going to try to put it at the top of main.rs because it looks like Rust code:
extern crate chrono;
fn main() {}Copy the code
Compile:
$cargo run Updating Registry 'https://github.com/rust-lang/crates.io-index' Downloading Num v0.1.25 Downloading RAND V0.3.8 Downloading chrono v0.2.14 Compiling Rand V0.3.8 Compiling Num v0.1.25 Compiling chrono v0.2.14 Compiling Simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) Running `/Users/joel/Projects/simple-log/target/debug/simple-log`Copy the code
Well, it seems to have downloaded Chrono, compiled and finished. I think the next step is to try it out. Based on the first example of the readme, I would like to go like this:
extern crate chrono;
use chrono::*;
fn main() {
let local: DateTime<Local> = Local::now(); println! ('{}'.local);
}
Copy the code
= >
$cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) main. Rs: now: when the error: unterminated character constant:'{ main.rs:6 println! ('{}', local); ^~ Could not compile `simple-log`. To learn more, run the command again with --verbose.Copy the code
… ? After a few stunned seconds, I realized that it was telling me that I should have used double quotes, not single quotes. It makes sense that single quotes are used for the lifecycle specification.
After switching from single quotes to double quotes:
$cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) Running ` / Users/Joel/Projects/simple - log/target/debug/simple - log ` 16:54:47. 2015-06-05 483088-04:00Copy the code
… Wow, that’s easy. It looks println! You can call some kind of interface to print all kinds of different things.
It’s ironic. I could easily build a simple “Hello World” class Web application and print a well-formed time, but I spent a lot of time writing files. I don’t know what that means. Although the Rust language is difficult to use (for me), I believe the Rust community has done a lot to make the system package work well.
3 Write the date and time to the file
I think the next step is to write the string to a file. To that end, I want to look at the end of my last post:
$ cat main_file_writing.rs
use std::io::prelude::*;
use std::fs::File;
use std::io;
fn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> {
letmut f = try! (File::create(filename)); try! (f.write_all(string)); Ok(()) } fnmain() {
match log_something("log.txt", b"ITS ALIVE!!!") { Ok(..) => println! ("File created!"), Err(..) => println! ("Error: could not create file.")}}Copy the code
I’m just combining the above example with this one:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_something(filename: &'static str, string: &'static [u8; 12]) -> io::Result<()> {
letmut f = try! (File::create(filename)); try! (f.write_all(string)); Ok(()) } fnmain() {
let local: DateTime<Local> = Local::now(); println! ('{}'.local);
match log_something("log.txt", b"ITS ALIVE!!!") { Ok(..) => println! ("File created!"), Err(..) => println! ("Error: could not create file.")}}Copy the code
Compile:
$ ls
Cargo.lock Cargo.toml log.txt src target
$ pwd/Users/joel/Projects/simple-log $ ls Cargo.lock Cargo.toml log.txt src target $ rm log.txt $ cargo run Compiling Simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) Running ` target/debug/simple - log ` 17:08:57. 2015-06-05 814176 -04:00 File created! $ cat log.txt ITS ALIVE!!! $Copy the code
It works! It’s fun to struggle with language, to put two things together so smoothly.
4 Build the time logger
We are getting closer to writing a real, complete, final system. It occurred to me that I could write some tests for this code, but not in a minute.
Here’s what this function should do:
- Given a filename,
- If it doesn’t exist, create it, and then open the file.
- Create a time and date string,
- Write the string to a file, then close the file.
4.1 tou8
The misunderstanding of
My first attempt:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_time(filename: &'static str) -> io::Result<()> { let local: DateTime
= Local::now(); let time_str = local.format("%Y").to_string(); let mut f = try! (File::create(filename)); try! (f.write_all(time_str)); Ok(()) } fn main() { match log_time("log.txt") { Ok(..) => println! ("File created!" ), Err(..) => println! ("Error: could not create file.") } }
Copy the code
= >
$cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) SRC/main. Rs: and: 13:30 error: mismatched types: expected `&[u8]`, found `collections::string::String` (expected &-ptr, found struct `collections::string::String`) [E0308] src/main.rs:13 try! (f.write_all(time_str)); ^~~~~~~~ <std macros>:1:1: 6:48 note:in expansion of try!
src/main.rs:13:5: 13:33 note: expansion site
error: aborting due to previous error
Could not compile `simple-log`.
To learn more, run the command again with --verbose.
Copy the code
I know there are a lot of string types 1 in Rust, and it looks like I need another type here. I didn’t know how to do it, so I had to do some searching.
I remember one section of the Rust document specifically mentioning strings. Look it up. It says you can convert String to & STR using ampersand. I feel this is not what we need because it should be a type conflict between [u8] and &str2. Let’s try it:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_time(filename: &'static str) -> io::Result<()> { let local: DateTime
= Local::now(); let time_str = local.format("%Y").to_string(); let mut f = try! (File::create(filename)); try! (f.write_all(&time_str)); Ok(()) } fn main() { match log_time("log.txt") { Ok(..) => println! ("File created!" ), Err(..) => println! ("Error: could not create file.") } }
Copy the code
= >
$cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) SRC/main. Rs: and, therefore, the error: mismatched types: expected `&[u8]`, found `&collections::string::String` (expected slice, found struct `collections::string::String`) [E0308] src/main.rs:13 try! (f.write_all(&time_str)); ^~~~~~~~~ <std macros>:1:1: 6:48 note:in expansion of try!
src/main.rs:13:5: 13:34 note: expansion site
error: aborting due to previous error
Could not compile `simple-log`.
To learn more, run the command again with --verbose.
Copy the code
Well, obviously, adding an ampersand can only convert from String to ampersand String. This seems to directly contradict what the Rust documentation says, but it could also be that I don’t know what’s going on.
… And I just read the end of the string chapter. As far as I know, there’s nothing here.
I went away for a while to work on other things (family matters, you know), and when I left, I had an Epiphany. Up until now, I thought U8 stood for UTF-8, but now that I think about it, it must stand for “unsigned 8-bit integer.” And I remember seeing the as_bytes method, so let’s try it:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_time(filename: &'static str) -> io::Result<()> { let local: DateTime
= Local::now(); let bytes = local.format("%Y").to_string().as_bytes(); let mut f = try! (File::create(filename)); try! (f.write_all(bytes)); Ok(()) } fn main() { match log_time("log.txt") { Ok(..) => println! ("File created!" ), Err(..) => println! ("Error: could not create file.") } }
Copy the code
= >
$cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) main. Rs: 10:17: when the error: borrowed value does not live long enough main.rs:10let bytes = local.format("%Y").to_string().as_bytes();
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.rs:10:59: 14:2 note: reference must be valid for the block suffix following statement 1 at 10:
58...
main.rs:10 let bytes = local.format("%Y").to_string().as_bytes();
main.rs:11 letmut f = try! (File::create(filename)); main.rs:12 try! (f.write_all(bytes)); main.rs:13 Ok(()) main.rs:14 } main.rs:10:5: 10:59 note: ... but borrowed value is only validfor the statement at 10:4
main.rs:10 let bytes = local.format("%Y").to_string().as_bytes();
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.rs:10:5: 10:59 help: consider using a `let` binding to increase its lifetime
main.rs:10 let bytes = local.format("%Y").to_string().as_bytes();
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `simple-log`.
To learn more, run the command again with --verbose.
Copy the code
Well, I hope things work out. Does this error mean THAT I fixed something and that there is some other error covering up the problem? Do I have a whole new problem?
The error messages are oddly concentrated on the same line. I don’t really understand it, but I think it’s telling me that I need to add an assignment statement to the method. Let’s try:
fn log_time(filename: &'static str) -> io::Result<()> { let local: DateTime
= Local::now(); let formatted = local.format("%Y").to_string(); let bytes = formatted.as_bytes(); let mut f = try! (File::create(filename)); try! (f.write_all(bytes)); Ok(()) }
Copy the code
= >
$cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) Running ` target/debug/simple - log ` File created! $ cat log.txt 2015$Copy the code
That’s great! Everything we want is right here. Before I go on, I want to tease you that I’m a little disappointed. Rust should be able to infer correct behavior from context without my prompting.
Test script:
$ ls
Cargo.lock Cargo.toml log.txt src target
$ rm log.txt
$ cargo run
Running `target/debug/simple-log`
File created!
$ cat log.txt
2015$ cargo run
Running `target/debug/simple-log`
File created!
$ cat log.txt
2015$
Copy the code
4.2 Check and fill in the gaps
Some questions:
- I can’t stand it without another line.
- Formatting takes some work.
- The new date will override the old one.
Let’s verify #3 by fixing the format. If the time changes between runs, then we will know that’s what is happening.
The format method in DateTime uses the standard Strftime format convention. Ideally, I’d like time to look something like this:
Sat, Jun 6 2015 05:32:00 PM
Sun, Jun 7 2015 08:35:00 AM
Copy the code
… And so on. This should be readable enough for me to use. After consulting the documentation, I came up with this:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_time(filename: &'static str) -> io::Result<()> { let local: DateTime
= Local::now(); let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string(); let bytes = formatted.as_bytes(); let mut f = try! (File::create(filename)); try! (f.write_all(bytes)); Ok(()) } fn main() { match log_time("log.txt") { Ok(..) => println! ("File created!" ), Err(..) => println! ("Error: could not create file.") } }
Copy the code
Testing:
$ rm log.txt
$ cargo run
Running `target/debug/simple-log`
File created!
$ cat log.txt
Sun, Jun 07 2015 06:37:21 PM
$ sleep 5; cargo run
Running `target/debug/simple-log`
File created!
$ cat log.txt
Sun, Jun 07 2015 06:37:41 PM
Copy the code
Obviously, the program overwrites the log entry I want. I remember the documentation in File:: Create pointed out what happened here. So, I need to look at the document again in order to process it properly.
I did some searching, and the answers were mostly inconsequential. I then found the documentation for STD ::Path ::Path, which has an EXISTS schema.
At this point, type conversions in my program became increasingly unmanageable. I feel nervous, so before continuing, I’m going to submit once.
I want to take the time entity string processing logic out of the log_time function, because time creation and formatting are obviously different from file manipulation code. So, I tried the following:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn log_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn log_time(filename: &'static str) -> io::Result<()> { let bytes = log_time_entry().as_bytes(); let mut f = try! (File::create(filename)); try! (f.write_all(bytes)); Ok(()) } fn main() { match log_time("log.txt") { Ok(..) => println! ("File created!" ), Err(..) => println! ("Error: could not create file.") } }Copy the code
= >
$cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) SRC/main. Rs: blessing: direction for the error: borrowed value does not live long enough src/main.rs:16let bytes = log_time_entry().as_bytes();
^~~~~~~~~~~~~~~~
src/main.rs:16:45: 20:2 note: reference must be valid for the block suffix following statement 0 at
16:44...
src/main.rs:16 let bytes = log_time_entry().as_bytes();
src/main.rs:17 letmut f = try! (File::create(filename)); src/main.rs:18 try! (f.write_all(bytes)); src/main.rs:19 Ok(()) src/main.rs:20 } src/main.rs:16:5: 16:45 note: ... but borrowed value is only validfor the statement at 16:4
src/main.rs:16 let bytes = log_time_entry().as_bytes();
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
src/main.rs:16:5: 16:45 help: consider using a `let` binding to increase its lifetime
src/main.rs:16 let bytes = log_time_entry().as_bytes();
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
error: aborting due to previous error
Could not compile `simple-log`.
To learn more, run the command again with --verbose.
Copy the code
Okay, this looks like a problem I’ve had before. Does it pretend or hold that the function has an explicit resource reference? It seems a little strange. I tried again to fix it:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn formatted_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn log_time(filename: &'static str) -> io::Result<()> { let entry = formatted_time_entry(); let bytes = entry.as_bytes(); let mut f = try! (File::create(filename)); try! (f.write_all(bytes)); Ok(()) } fn main() { match log_time("log.txt") { Ok(..) => println! ("File created!" ), Err(..) => println! ("Error: could not create file.") } }Copy the code
= >
$cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) Running ` target/debug/simple - log ` File created!Copy the code
So, it seems that adding an explicit reference solves the problem. Anyway, the rule is pretty simple.
Next, I’ll extract the file manipulation code into its own function:
extern crate chrono;
use std::io::prelude::*;
use std::fs::File;
use std::io;
use chrono::*;
fn formatted_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {
letmut f = try! (File::create(filename)); try! (f.write_all(bytes)); Ok(()) } fn log_time(filename: &'static str) -> io::Result<()> { let entry = formatted_time_entry(); let bytes = entry.as_bytes(); try! (record_entry_in_log(filename, &bytes)); Ok(()) } fn main() { match log_time("log.txt") { Ok(..) => println! ("File created!" ), Err(..) => println! ("Error: could not create file.") } }Copy the code
It works. I made some initial mistakes, but they were quickly corrected. Here is the modified code.
Looking at STD ::fs::File in the documentation, I noticed that the documentation says STD ::fs::OpenOptions, which is exactly what I was looking for. This is certainly better than using STD :: Path.
My first attempt:
extern crate chrono;
use std::io::prelude::*;
use std::fs::{File,OpenOptions};
use std::io;
use chrono::{DateTime,Local};
fn formatted_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {
letmut file = try! (OpenOptions::new(). append(true).
create(true). open(filename)); try! (file.write_all(bytes)); Ok(()) } fn log_time(filename: &'static str) -> io::Result<()> { let entry = formatted_time_entry(); let bytes = entry.as_bytes(); try! (record_entry_in_log(filename, &bytes)); Ok(()) } fn main() { match log_time("log.txt") { Ok(..) => println! ("File created!" ), Err(..) => println! ("Error: could not create file.") } }Copy the code
= >
$cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) SRC/main. Rs: 4:15:4:19 warning: unused import,#[warn(unused_imports)] on by default
src/main.rs:4 use std::fs::{File,OpenOptions};
^~~~
Running `target/debug/simple-log`
Error: could not create file.
Copy the code
Interesting. It actually created the file successfully. Oh, I notice that the error message is a message that I hard-coded into Main. I think this way it will work:
extern crate chrono;
use std::io::prelude::*;
use std::fs::{File,OpenOptions};
use std::io;
use chrono::{DateTime,Local};
fn formatted_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {
letmut file = try! (OpenOptions::new(). append(true).
create(true). open(filename)); try! (file.write_all(bytes)); Ok(()) } fn log_time(filename: &'static str) -> io::Result<()> { let entry = formatted_time_entry(); let bytes = entry.as_bytes(); try! (record_entry_in_log(filename, &bytes)); Ok(()) } fn main() { match log_time("log.txt") { Ok(..) => println! ("File created!" ), Err(e) => println! ("Error: {}", e) } }Copy the code
= >
$cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) SRC/main. Rs: 4:15:4:19 warning: unused import,#[warn(unused_imports)] on by default
src/main.rs:4 use std::fs::{File,OpenOptions};
^~~~
Running `target/debug/simple-log`
Error: Bad file descriptor (os error 9)
Copy the code
Strange. A search for the “invalid file description” error message seems to indicate that the file description being used has been turned off. What happens if I comment out the file.write_all call?
$rm log. TXT $cargo run the Compiling simple - log v0.1.0 (file:///Users/joel/Projects/simple-log) SRC/main. Rs: 3:5: 3:25 warning: unused import,#[warn(unused_imports)] on by default
src/main.rs:3 use std::io::prelude::*;
^~~~~~~~~~~~~~~~~~~~
src/main.rs:4:15: 4:19 warning: unused import, #[warn(unused_imports)] on by default
src/main.rs:4 use std::fs::{File,OpenOptions};
^~~~
src/main.rs:15:40: 15:45 warning: unused variable: `bytes`, #[warn(unused_variables)] on by default
src/main.rs:15 fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {
^~~~~
src/main.rs:16:9: 16:17 warning: unused variable: `file`, #[warn(unused_variables)] on by default
src/main.rs:16 letmut file = try! (OpenOptions::new(). ^~~~~~~~ src/main.rs:16:9: 16:17 warning: variable does not need to be mutable,#[warn(unused_mut)] on by de
fault
src/main.rs:16 letmut file = try! (OpenOptions::new(). ^~~~~~~~ Running `target/debug/simple-log` File created! $ ls Cargo.lock Cargo.toml log.txt src targetCopy the code
As expected, there were a bunch of unused warnings, but the file was created without them.
This seems silly, but after I tried adding.write(true) to the function call chain, it worked. Semantically.append(true) means.write(true), but I don’t think that’s technically the case.
Got this! It’s working! Final version:
extern crate chrono;
use std::io::prelude::*;
use std::fs::{File,OpenOptions};
use std::io;
use chrono::{DateTime,Local};
fn formatted_time_entry() -> String {
let local: DateTime<Local> = Local::now();
let formatted = local.format("%a, %b %d %Y %I:%M:%S %p\n").to_string();
formatted
}
fn record_entry_in_log(filename: &str, bytes: &[u8]) -> io::Result<()> {
letmut file = try! (OpenOptions::new(). append(true).
write(true).
create(true). open(filename)); try! (file.write_all(bytes)); Ok(()) } fn log_time(filename: &'static str) -> io::Result<()> { let entry = formatted_time_entry(); let bytes = entry.as_bytes(); try! (record_entry_in_log(filename, &bytes)); Ok(()) } fn main() { match log_time("log.txt") { Ok(..) => println! ("File created!" ), Err(e) => println! ("Error: {}", e) } }Copy the code
= >
$ ls
Cargo.lock Cargo.toml src target
$ cargo run
Running `target/debug/simple-log`
File created!
$ cargo run
Running `target/debug/simple-log`
File created!
$ cat log.txt
Sun, Jun 07 2015 10:40:01 PM
Sun, Jun 07 2015 10:40:05 PM
Copy the code
5 Conclusions & Follow-up steps
Rust is getting easier for me. I now have some working, single-function code to work with, and I feel pretty confident about the development of the next part of the program.
When I first planned this series, I planned that my next task would be to integrate logging code with nickel.rs code, but now I think it’s pretty simple. My guess is that the next challenging part will be dealing with option parsing.
–
Article series: Developing a Simple Web application using Rust
- Part 1
- The second part a
- The second part b
- Part 3
- Part 4
- conclusion
Footnote:
It makes sense that 1 has many kinds of strings. A string is a complex entity that is difficult to express properly. Unfortunately, strings seem so simple at first glance that this sort of thing doesn’t seem to need to be complicated.
I don’t know what I’m talking about. That is all that is attainable now.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.