A documentação oficial do Rust é formidável, porém a explicação sobre a estruturação de arquivos e diretórios pode parecer confusa. Nesse artigo pretendo mostrar de maneira simples - sem se aprofundar em todas possibilidades e detalhes. Vamos conferir!
Em Rust os componentes fn
, struct
, enum
, trait
, etc. são chamados de itens.
Os itens por suas vez podem ser agrupados em módulos mod
. Os módulos também são considerados itens, então formam uma estrutura hierárquica.
Criando um outro fonte
Vamos criar um programa vazio, entrar no diretório e abrir o vs code
:
cargo new struct-dir
cd struct-dir
code .
Quando criamos um projeto novo temos a seguinte estrutura:
No main.rs
há a função de Hello World:
fn main() {
println!("Hello, world!");
}
Legal, agora vamos criar uma nova função say_hello
e chamá-la do main()
:
fn main() {
say_hello();
}
fn say_hello() {
println!("Hello, world!");
}
Suponha que queremos colocar o método say_hello
em um arquivo chamado services.rs
.
A estrutura ficará assim:
Ao compilarmos agora temos o erro:
error[E0425]: cannot find function `say_hello` in this scope
--> src/main.rs:2:5
|
2 | say_hello();
| ^^^^^^^^^ not found in this scope
Para resolver isso temos que fazer mais algumas coisas.
Primeiro, temos que tornar a função say_hello
pública, ou seja, ser visível para funções fora do seu código fonte.
Para isso basta informar o prefixo pub
antes de fn
:
pub fn say_hello() {
println!("Hello, world!");
}
Segundo, temos que indicar que o arquivo services.rs
será um módulo. Para isso temos que inserir no início do arquivo main.rs
a linha mod services;
Agora podemos informar o nome do módulo no prefixo da função, usando o delimitador ::
. O código ficará assim:
mod services;
fn main() {
services::say_hello();
}
Pronto, agora o projeto compila e executa corretamente.
Porém pode ficar muito extenso informar o nome do módulo antes da função. Para evitar isso é possível fazer o import desse módulo e função, onde inserimos no início do fonte a linha use services::say_hello;
:
mod services;
use services::say_hello;
fn main() {
say_hello();
}
O
main.rs
é um arquivo importante dentro do diretóriosrc
, no cabeçalho ele define os módulos que este diretório contém.
A seguir criaremos uma nova função dentro do services.rs
:
pub fn say_goodbye() {
println!("Goodbye!");
}
E vamos chamar no main
:
mod services;
use services::say_goodbye;
use services::say_hello;
fn main() {
say_hello();
say_goodbye();
}
Quando importamos funções de mesmo módulo podemos "agrupar" o módulo e distinguir as funções entre chaves {...}
. Por exemplo:
mod services;
use services::{say_goodbye, say_hello};
fn main() {
say_hello();
say_goodbye();
}
Vamos seguir e criar um arquivo chamado config.rs
, com o seguinte conteúdo:
pub struct Config {
pub user_name: String,
}
pub fn get_config() -> Config {
Config {
user_name: "Silva".to_string(),
}
}
Perceba que struct Config
e o campo user_name
possuem o pub
como prefixo, indicando eles podem ser acessível em outro fonte.
A estrutura ficará assim:
Agora vamos acessar no main.rs
:
mod config;
mod services;
use config::get_config;
use services::{say_goodbye, say_hello};
fn main() {
say_hello();
let config = get_config();
println!("{}", config.user_name);
say_goodbye();
}
Para poder acessar a função get_config
foi necessário inserir as linhas mod config;
e use config::get_config;
.
Criando subdiretório
Agora vamos supor que queremos criar entidades de domínio, mas queremos organizar num diretório novo, chamado domain
, dentro do src
.
Será criado o arquivo user.rs
e product.rs
:
O arquivo user.rs
terá o conteúdo:
pub struct Product {
pub product_name: String,
}
E o arquivo product.rs
terá:
pub struct Product {
pub product_name: String,
}
Certo, agora temos que indicar que o diretório domain
será um módulo, para isso voltamos a alterar o main.rs
e no cabeçalho vamos inserir a linha mod domain;
:
mod config;
mod services;
mod domain;
use config::get_config;
use services::{say_goodbye, say_hello};
// Resto do código
Porém ao tentar compilar temos um erro:
--> src/main.rs:3:1
|
3 | mod domain;
| ^^^^^^^^^^^
|
= help: to create the module `domain`, create file "src/domain.rs"
error: aborting due to previous error
O compilador espera que domain
seja um arquivo.
Para indicar que um diretório seja considerado um módulo precisamos criar um arquivo chamado
mod.rs
dentro do deste diretório:
Depois disso deve compilar corretamente.
A mesma palavra
mod
serve para definir módulo, tanto se for um arquivo ou diretório.
Acessando um arquivo em um subdiretório
Legal, agora vamos tentar acessar a entidade User
de dentro do main.rs
, inserindo no cabeçalho: use domain::user::User;
:
mod config;
mod services;
mod domain;
use config::get_config;
use domain::user::User;
use services::{say_goodbye, say_hello};
fn main() {
say_hello();
let config = get_config();
println!("{}", config.user_name);
let user = User {
user_name : config.user_name,
};
say_goodbye();
}
Porém ao compilar temos o seguinte erro:
--> src/main.rs:5:13
|
5 | use domain::user::User;
| ^^^^ could not find `user` in `domain`
error: aborting due to previous error
For more information about this error, try `rustc --explain E0432`.
O compilador entendeu que o diretório domain
é um módulo mas não entendeu que o arquivo user
também é um módulo.
Para isso precisamos indicar no mod.rs
do subdiretório domain
que user
é um módulo, então nele vamos inserir a linha:
pub mod user;
Pronto, agora compilou. O que fizemos no mod.rs
foi semelhante ao que fizemos no main.rs
.
Então perceba que, assim como
main.rs
definia os módulos (arquivos e diretórios que contém funções, objetos, etc), para os demais subdiretórios será o arquivomod.rs
que fará isso. Existe uma hierarquia, cada subdiretório poderá ter o seumod.rs
.
No arquivo mod.rs
também podemos definir funções, objetos, etc.
Acessando módulo em um nível anterior
Agora vamos supor que no arquivo user.rs
queremos acessar o objeto Config
.
Vamos tentar inserir use config::get_config;
e criar um método default
que retorna o usuário a partir da função get_config
:
use config::get_config;
pub struct User {
pub user_name: String,
}
impl User {
pub fn default() -> User {
User {
user_name : get_config().user_name,
}
}
}
Mas ao compilarmos temos o seguinte erro:
error[E0432]: unresolved import `config`
--> src/domain/user.rs:1:5
|
1 | use config::get_config;
| ^^^^^^ help: a similar path exists: `crate::config`
|
= note: `use` statements changed in Rust 2018; read more at <https://doc.rust-lang.org/edition-guide/rust-2018/module-system/path-clarity.html>
error: aborting due to previous error
Vimos que podemos acessar outras funções e objetos declarados em outros fontes, através do use
, acessando os módulos delimitados por ::
.
Porém essa via dos módulos que utilizamos até agora é relativa ao fonte atual. Se fazemos use config
no user.rs
o compilador tentará acessar esse módulo no diretório do user.rs
.
Observe que no erro acima o compilador já nos deu uma dica do que precisamos: crate::config
.
O prefixo crate
significa a via "raiz" do nosso projeto. Se alterarmos a linha para use crate::config::get_config;
então agora compila corretamente:
use crate::config::get_config;
pub struct User {
pub user_name: String,
}
impl User {
pub fn default() -> User {
User {
user_name : get_config().user_name,
}
}
}
Conclusão
Aqui vimos o funcionamento básico para criar módulos em Rust, distintos entre fontes no mesmo nível e em subdiretórios. Também foi mostrado como podemos acessar as funções de um módulo a outro.
Rust ainda permite abstrair a forma como os módulos são expostos, podendo ter os arquivos físicos numa estrutura diferente de como são publicados. Porém o objetivo deste artigo foi justamente mostrar uma forma simples que podemos organizar os fontes, que possivelmente será a maneira que utilizaremos na maioria das vezes.