Let's integrate prepared GraphQL helpers and queries from the previous chapter to our pages.
We can start with the simplest page - src/page/clients_and_projects.rs.
clients_and_projectsImport required items:
use ulid::Ulid;
use cynic::QueryBuilder; // <-- New
use std::collections::BTreeMap;
use std::convert::identity; // <-- New
use crate::graphql; // <-- New
type ClientId = Ulid;
cynic::QueryBuilder to call the build function in send_operation(MyQuery::build(()).|x| x, however I recommend to read the docs to know the differences and where it's useful.Change FetchError to GraphQLError in errors:
pub struct Model {
...
errors: Vec<graphql::GraphQLError>,
Change fetch::Result to graphql::Result in Msg::ClientsFetched. And we want to log clients and update Model on fetch:
pub enum Msg {
ClientsFetched(graphql::Result<BTreeMap<ClientId, Client>>),
...
pub fn update ... {
match msg {
Msg::ClientsFetched(Ok(clients)) => {
log!("Msg::ClientsFetched", clients);
model.clients = RemoteData::Loaded(clients);
},
Msg::ClientsFetched(Err(graphql_error)) => {
model.errors.push(graphql_error);
},
And we have to derive Debug for some items because of our new log! call:
#[derive(Debug)]
pub struct Client {
...
#[derive(Debug)]
struct Project {
...
Send GraphQL query on init and set client state to Loading:
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
orders.perform_cmd(async { Msg::ClientsFetched(request_clients().await) });
Model {
...
clients: RemoteData::Loading,
And the last and most important thing just below the init function - request_clients:
async fn request_clients() -> graphql::Result<BTreeMap<ClientId, Client>> {
use graphql::queries::clients_with_projects as query_mod;
let project_mapper = |project: query_mod::Project| (
project.id.parse().expect("parse project Ulid"),
Project { name: project.name }
);
let client_mapper = |client: query_mod::Client| (
client.id.parse().expect("parse client Ulid"),
Client {
name: client.name,
projects: client.projects.into_iter().map(project_mapper).collect()
}
);
Ok(
graphql::send_operation(query_mod::Query::build(()))
.await?
.query_client
.expect("get clients")
.into_iter()
.filter_map(identity)
.map(client_mapper)
.collect()
)
}
The purpose of this function is to send a GraphQL request and then return response data to fill our Model later. However there is a problem - we can't just move the response data directly to our Model because they have different types. So we have to transform response data to Model data by *_mapper closures.
query_mod::Query::build(()) creates an Operation with no Arguments (represented by unit ()).
We need to call .expect("get clients") because query_client is Option. And it's Option because Slash GraphQL generated the function queryClient with optional array of Clients and then cynic forced us to respect that because of the schema.graphql.
We need to call filter_map(identity) to remove potential None values from query_client list. It's also caused by the generated queryClient function type.
We've written mappers as closures inside request_clients() body to not pollute the file by functions that are used only in once place. Also it plays nicely with our alias use graphql::queries::clients_with_projects as query_mod. We can always refactor it to make mappers reusable and to respect the rule "children below the parent".
time_trackerIt's very similar to the previous page.
Import required items:
use ulid::Ulid;
use cynic::QueryBuilder; // <-- New
use std::collections::BTreeMap;
use std::convert::identity; // <-- New
use crate::graphql; // <-- New
type ClientId = Ulid;
Change FetchError to GraphQLError in errors:
pub struct Model {
...
errors: Vec<graphql::GraphQLError>,
Change fetch::Result to graphql::Result in Msg::ClientsFetched. And we want to log clients and update Model on fetch:
pub enum Msg {
ClientsFetched(graphql::Result<BTreeMap<ClientId, Client>>),
...
pub fn update ... {
match msg {
Msg::ClientsFetched(Ok(clients)) => {
log!("Msg::ClientsFetched", clients);
model.clients = RemoteData::Loaded(clients);
},
Msg::ClientsFetched(Err(graphql_error)) => {
model.errors.push(graphql_error);
},
And we have to derive Debug for some items because of our new log! call:
#[derive(Debug)]
pub struct Client {
...
#[derive(Debug)]
struct Project {
...
#[derive(Debug)]
struct TimeEntry {
...
Send GraphQL query on init and set client state to Loading:
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
orders.perform_cmd(async { Msg::ClientsFetched(request_clients().await) });
Model {
...
clients: RemoteData::Loading,
And the last and most important thing just below the init function - request_clients:
async fn request_clients() -> graphql::Result<BTreeMap<ClientId, Client>> {
use graphql::queries::clients_with_projects_with_time_entries as query_mod;
let time_entry_mapper = |time_entry: query_mod::TimeEntry| (
time_entry.id.parse().expect("parse time_entry Ulid"),
TimeEntry {
name: time_entry.name,
started: time_entry.started.0.parse().expect("parse time_entry started time"),
stopped: time_entry.stopped.map(|time| time.0.parse().expect("parse time_entry started time")),
}
);
let project_mapper = |project: query_mod::Project| (
project.id.parse().expect("parse project Ulid"),
Project {
name: project.name,
time_entries: project.time_entries.into_iter().map(time_entry_mapper).collect()
},
);
let client_mapper = |client: query_mod::Client| (
client.id.parse().expect("parse client Ulid"),
Client {
name: client.name,
projects: client.projects.into_iter().map(project_mapper).collect()
}
);
Ok(
graphql::send_operation(query_mod::Query::build(()))
.await?
.query_client
.expect("get clients")
.into_iter()
.filter_map(identity)
.map(client_mapper)
.collect()
)
}
time_blocksImport required items:
use ulid::Ulid;
use cynic::QueryBuilder; // <-- New
use std::collections::BTreeMap;
use std::convert::identity; // <-- New
use std::ops::Add; // <-- New
use crate::graphql; // <-- New
type ClientId = Ulid;
MyType::add instead of a closure |x| x + x to a function that expects a function as an argument. It'll make our code more readable and declarative.Change FetchError to GraphQLError in errors:
pub struct Model {
...
errors: Vec<graphql::GraphQLError>,
Change fetch::Result to graphql::Result in Msg::ClientsFetched. And we want to log clients and update Model on fetch:
pub enum Msg {
ClientsFetched(graphql::Result<BTreeMap<ClientId, Client>>),
...
pub fn update ... {
match msg {
Msg::ClientsFetched(Ok(clients)) => {
log!("Msg::ClientsFetched", clients);
model.clients = RemoteData::Loaded(clients);
},
Msg::ClientsFetched(Err(graphql_error)) => {
model.errors.push(graphql_error);
},
And we have to derive Debug for some items because of our new log! call:
#[derive(Debug)]
pub struct Client {
...
#[derive(Debug)]
struct TimeBlock {
...
#[derive(Debug)]
pub enum TimeBlockStatus {
...
#[derive(Debug)]
struct Invoice {
...
Send GraphQL query on init and set client state to Loading:
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
orders.perform_cmd(async { Msg::ClientsFetched(request_clients().await) });
Model {
...
clients: RemoteData::Loading,
And the last and most important thing just below the init function - request_clients:
async fn request_clients() -> graphql::Result<BTreeMap<ClientId, Client>> {
use graphql::queries::clients_with_time_blocks_and_time_entries as query_mod;
let invoice_mapper = |invoice: query_mod::Invoice| {
Invoice {
id: invoice.id.parse().expect("parse invoice Ulid"),
custom_id: invoice.custom_id,
url: invoice.url,
}
};
let status_mapper = |status: query_mod::TimeBlockStatus| {
match status {
query_mod::TimeBlockStatus::NonBillable => TimeBlockStatus::NonBillable,
query_mod::TimeBlockStatus::Unpaid => TimeBlockStatus::Unpaid,
query_mod::TimeBlockStatus::Paid => TimeBlockStatus::Paid,
}
};
let time_block_mapper = |time_block: query_mod::TimeBlock| (
time_block.id.parse().expect("parse time_block Ulid"),
TimeBlock {
name: time_block.name,
status: status_mapper(time_block.status),
duration: Duration::seconds(i64::from(time_block.duration)),
invoice: time_block.invoice.map(invoice_mapper),
}
);
let compute_tracked_time = |projects: Vec<query_mod::Project>| {
projects
.into_iter()
.flat_map(|project| project.time_entries)
.map(|time_entry| {
let started: DateTime<Local> =
time_entry.started.0.parse().expect("parse time_entry started");
let stopped: DateTime<Local> = if let Some(stopped) = time_entry.stopped {
stopped.0.parse().expect("parse time_entry stopped")
} else {
chrono::Local::now()
};
stopped - started
})
.fold(Duration::seconds(0), Duration::add)
};
let client_mapper = |client: query_mod::Client| (
client.id.parse().expect("parse client Ulid"),
Client {
name: client.name,
time_blocks: client.time_blocks.into_iter().map(time_block_mapper).collect(),
tracked: compute_tracked_time(client.projects),
}
);
Ok(
graphql::send_operation(query_mod::Query::build(()))
.await?
.query_client
.expect("get clients")
.into_iter()
.filter_map(identity)
.map(client_mapper)
.collect()
)
}
Iterator methods in the closure compute_tracked_time. I recommend to read their docs if you are not an experienced functional programmer:Let's finally test how our app sends and decodes queries:
![]()
Nice! We can start to write view functions in the next chapter.