ferrovue 0.7.1

Vue templates compiled to Rust render functions: server-side rendering with no JavaScript at run time
Documentation
Responding with a render from axum or actix-web.

Three optional features turn renders into HTTP responses:

| Feature | What it adds |
|---|---|
| `axum` | `IntoResponse` for [`Html`]crate::Html and `HtmlStream` |
| `actix-web` | `Responder` for both, and `From` both for `HttpResponse` |
| `stream` | `HtmlStream` alone, a `Stream` of byte chunks for any other server; both features above include it |

```toml
[dependencies]
ferrovue = { version = "0.7", features = ["axum"] }
```

Every response is `200 OK` with `Content-Type: text/html; charset=utf-8`; each framework's own way
of changing the status applies.

# A component

A generated `html()` or `island()` is an [`Html`](crate::Html), and with the `axum` feature an
`IntoResponse`. A handler builds the props and returns `into_html(props)` or `into_island(props)`,
the forms that hold their props: the component is rendered once the handler has returned, into the
response's body.

```rust
# mod greeting {
# // @generated by ferrovue from components/Greeting.vue. Do not edit: change the `.vue` file and run
# // `ferrovue`.
#
# use std::borrow::Cow;
#
# use ferrovue as fv;
#
# /// The component's name, as `data-island` carries it.
# pub const NAME: &str = "Greeting";
#
# /// The props `Greeting.vue` declares.
# #[derive(Debug, Clone, serde::Serialize)]
# #[cfg_attr(test, derive(serde::Deserialize))]
# pub struct Props<'a> {
#     #[serde(rename = "name")]
#     pub name: Cow<'a, str>,
# }
#
# impl<'a> Props<'a> {
#     /// Props with its required fields.
#     pub fn new(name: impl Into<Cow<'a, str>>) -> Self {
#         Props { name: name.into() }
#     }
#
# }
#
#
# /// Write the component's server render into `out`.
# pub fn render(out: &mut String, props: &Props<'_>) {
#     out.reserve(30 + props.name.len());
#     out.push_str("<p class=\"greeting\">Hello, ");
#     fv::escape_into(out, &props.name);
#     out.push_str("!</p>");
# }
#
# /// The component's markup, for a maud page that shows it without hydrating it.
# pub fn html<'p, 'a>(props: &'p Props<'a>) -> fv::Html<'p, Props<'a>> {
#     fv::Html::markup(props, render)
# }
#
# /// The component as an island the client hydrates.
# pub fn island<'p, 'a>(props: &'p Props<'a>) -> fv::Html<'p, Props<'a>> {
#     fv::Html::island(NAME, props, render)
# }
#
# /// [`html`], holding the props: a handler that builds them can return it.
# pub fn into_html<'a>(props: Props<'a>) -> fv::Html<'a, Props<'a>> {
#     fv::Html::markup_owned(props, render)
# }
#
# /// [`island`], holding the props.
# pub fn into_island<'a>(props: Props<'a>) -> fv::Html<'a, Props<'a>> {
#     fv::Html::island_owned(NAME, props, render)
# }
# }
# #[cfg(feature = "axum")]
# fn main() {
use axum::Router;
use axum::extract::Path;
use axum::response::IntoResponse;
use axum::routing::get;

async fn hello(Path(name): Path<String>) -> impl IntoResponse {
    greeting::into_html(greeting::Props::new(name))
}

// The island alone, for a client that swaps it into a page it already shows.
async fn hello_island(Path(name): Path<String>) -> impl IntoResponse {
    greeting::into_island(greeting::Props::new(name))
}

let app: Router = Router::new()
    .route("/hello/{name}", get(hello))
    .route("/islands/hello/{name}", get(hello_island));
# let response = tokio::runtime::Builder::new_current_thread().build().unwrap()
#     .block_on(hello(Path("<Ada>".to_owned()))).into_response();
# assert_eq!(response.status(), 200);
# assert_eq!(response.headers()["content-type"], "text/html; charset=utf-8");
# let body = tokio::runtime::Builder::new_current_thread().build().unwrap()
#     .block_on(axum::body::to_bytes(response.into_body(), usize::MAX)).unwrap();
# assert_eq!(body, r#"<p class="greeting">Hello, &lt;Ada&gt;!</p>"#);
# }
# #[cfg(not(feature = "axum"))]
# fn main() {}
```

The response outlives the handler, so the `Html` must borrow nothing from it: props of owned
strings (`Props::new(name)` with a `String`) or `'static` ones. `html(&props)` and `island(&props)`
borrow the props, and serve as a response when the props outlive the handler, such as a `static`.
A component that takes slots, the route, stores, translations or teleports has only `html`, which
borrows them as well; render it into the page's `String` and respond with that, as below.

# A page

A whole page is usually written into one `String`: the document's head, the layout's render with
the route's page in its `<RouterView>` slot, the stores' state, the client's script. Make it an
`HtmlStream` to send it. A render without holes is sent whole, with its `Content-Length`; for
another status than `200 OK`, respond with the status beside it:

```rust
# #[cfg(feature = "axum")]
# fn main() {
use axum::http::{StatusCode, Uri};
use axum::response::{IntoResponse, Response};
use ferrovue::HtmlStream;

async fn not_found(uri: Uri) -> Response {
    let mut page = String::from("<!doctype html><html><body><h1>Nothing at <code>");
    ferrovue::escape_into(&mut page, uri.path());
    page.push_str("</code></h1></body></html>");
    (StatusCode::NOT_FOUND, HtmlStream::new(page)).into_response()
}

let app: axum::Router = axum::Router::new().fallback(not_found);
# }
# #[cfg(not(feature = "axum"))]
# fn main() {}
```

# A page streamed

A page rendered with [`hole`](crate::hole)s ([`streaming`](crate::guide::streaming)) is sent in
pieces. Give `HtmlStream` the whole render and a future for each hole, in the order the holes
appear: the markup up to the first hole goes out at once, and each hole's content, then the markup
after it, as soon as that hole and every one before it are ready. The futures all run from the
start, so the page ends when the slowest one does.

```rust
# mod reviews {
# use ferrovue as fv;
# #[derive(serde::Serialize)]
# pub struct Props { pub count: i64 }
# pub fn render(out: &mut String, props: &Props) {
#     out.push_str("<p>");
#     fv::push_int(out, props.count);
#     out.push_str(" reviews</p>");
# }
# pub fn into_island(props: Props) -> fv::Html<'static, Props> {
#     fv::Html::island_owned("Reviews", props, render)
# }
# }
# #[cfg(feature = "axum")]
# fn main() {
use std::time::Duration;

use axum::extract::Path;
use axum::response::{IntoResponse, Response};
use ferrovue::{HtmlStream, hole, slot_into};

/// The reviews of a book, from a slow service.
async fn review_count(_id: &str) -> i64 {
    tokio::time::sleep(Duration::from_millis(10)).await;
    3
}

async fn book(Path(id): Path<String>) -> Response {
    // The book's page, rendered at once with a hole where the reviews go: here the slot of a
    // generated component, `Slots { reviews: Some(ferrovue::hole()), .. }`.
    let mut page = String::from("<!doctype html><html><body><h1>");
    ferrovue::escape_into(&mut page, &id);
    page.push_str("</h1><section>");
    slot_into(&mut page, Some(hole()), None);
    page.push_str("</section></body></html>");

    // The hole's content: an island, rendered when its data is ready. The future owns what it
    // reads, as the response outlives the handler.
    let reviews = async move {
        let props = reviews::Props { count: review_count(&id).await };
        reviews::into_island(props).into_string()
    };
    HtmlStream::new(page).hole(reviews).into_response()
}
# let response = tokio::runtime::Builder::new_current_thread().enable_time().build().unwrap()
#     .block_on(async {
#         let response = book(Path("dune".to_owned())).await;
#         axum::body::to_bytes(response.into_body(), usize::MAX).await.unwrap()
#     });
# assert_eq!(
#     response,
#     concat!(
#         "<!doctype html><html><body><h1>dune</h1><section><!--[-->",
#         r#"<div data-island="Reviews" data-props="{&quot;count&quot;:3}"><p>3 reviews</p></div>"#,
#         "<!--]--></section></body></html>",
#     )
# );
# }
# #[cfg(not(feature = "axum"))]
# fn main() {}
```

`HtmlStream::holes` takes the futures from an iterator, when every
hole's content comes from the same function; `hole` takes one at a time, each of its own type. A
hole given no future is left empty, and a future beyond the last hole is never run. A future must
be `Send` and own what it reads (an `async move` block, an `Arc` of shared state cloned into it),
as the body is sent after the handler returns.

The repository's `examples/fullstack` serves every page this way, its book pages with their reviews
in a hole, and the reviews alone, at `/books/{id}/reviews`, from `into_island`.

# actix-web

The same with the `actix-web` feature: `Html` and `HtmlStream` are `Responder`s, and either becomes
an `HttpResponse` with `.into()`.

```rust
# mod greeting {
# use std::borrow::Cow;
# use ferrovue as fv;
# #[derive(Debug, Clone, serde::Serialize)]
# pub struct Props<'a> {
#     #[serde(rename = "name")]
#     pub name: Cow<'a, str>,
# }
# impl<'a> Props<'a> {
#     pub fn new(name: impl Into<Cow<'a, str>>) -> Self {
#         Props { name: name.into() }
#     }
# }
# pub fn render(out: &mut String, props: &Props<'_>) {
#     out.push_str("<p class=\"greeting\">Hello, ");
#     fv::escape_into(out, &props.name);
#     out.push_str("!</p>");
# }
# pub fn into_html<'a>(props: Props<'a>) -> fv::Html<'a, Props<'a>> {
#     fv::Html::markup_owned(props, render)
# }
# }
# #[cfg(feature = "actix-web")]
# fn main() {
use actix_web::http::StatusCode;
use actix_web::{App, Responder, web};
use ferrovue::HtmlStream;

async fn hello(name: web::Path<String>) -> impl Responder {
    greeting::into_html(greeting::Props::new(name.into_inner()))
}

async fn not_found() -> impl Responder {
    let page = String::from("<!doctype html><html><body><h1>Not found</h1></body></html>");
    HtmlStream::new(page).customize().with_status(StatusCode::NOT_FOUND)
}

let app = App::new()
    .route("/hello/{name}", web::get().to(hello))
    .default_service(web::to(not_found));
# }
# #[cfg(not(feature = "actix-web"))]
# fn main() {}
```

# Other servers

With the `stream` feature alone, `HtmlStream` is a `futures_core::Stream` of
`Result<bytes::Bytes, Infallible>`, which is what a Rust HTTP server makes a streamed body from:
hyper's `StreamBody`, once each chunk is mapped to a frame, or any framework's equivalent. Its
pieces are slices of the one render, never copied.