diff --git a/Cargo.toml b/Cargo.toml index bb6a39145d..4ebb84e256 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "packages/perseus", + "packages/perseus-actix-web", "examples/showcase/app", - "examples/showcase/server" + "examples/showcase/server-actix-web" ] \ No newline at end of file diff --git a/bonnie.toml b/bonnie.toml index a01b876bb9..c45742489f 100644 --- a/bonnie.toml +++ b/bonnie.toml @@ -5,10 +5,11 @@ dev.subcommands.build = [ "cd examples/showcase", "bonnie build --watch" ] -dev.subcommands.serve = [ +dev.subcommands.serve.cmd = [ "cd examples/showcase", - "bonnie serve" + "bonnie serve %server" ] +dev.subcommands.serve.args = [ "server" ] build = "cargo build" test = "cargo watch -x \"test\"" check = "cargo check && cargo fmt -- --check && cargo clippy && cargo test" # This will be run on CI as well diff --git a/examples/showcase/app/src/pages/about.rs b/examples/showcase/app/src/pages/about.rs index caaf6b6444..c193c30776 100644 --- a/examples/showcase/app/src/pages/about.rs +++ b/examples/showcase/app/src/pages/about.rs @@ -1,5 +1,6 @@ use perseus::template::Template; use sycamore::prelude::{component, template, GenericNode, Template as SycamoreTemplate}; +use std::sync::Arc; #[component(AboutPage)] pub fn about_page() -> SycamoreTemplate { @@ -13,7 +14,7 @@ pub fn get_page() -> Template { } pub fn template_fn() -> perseus::template::TemplateFn { - Box::new(|_| { + Arc::new(|_| { template! { AboutPage() } diff --git a/examples/showcase/app/src/pages/index.rs b/examples/showcase/app/src/pages/index.rs index c361bee455..24b1574969 100644 --- a/examples/showcase/app/src/pages/index.rs +++ b/examples/showcase/app/src/pages/index.rs @@ -2,6 +2,7 @@ use perseus::template::Template; use perseus::errors::ErrorCause; use serde::{Deserialize, Serialize}; use sycamore::prelude::{component, template, GenericNode, Template as SycamoreTemplate}; +use std::sync::Arc; #[derive(Serialize, Deserialize, Debug)] pub struct IndexPageProps { @@ -18,7 +19,7 @@ pub fn index_page(props: IndexPageProps) -> SycamoreTemplate { pub fn get_page() -> Template { Template::new("index") - .build_state_fn(Box::new(get_static_props)) + .build_state_fn(Arc::new(get_static_props)) .template(template_fn()) } @@ -30,7 +31,7 @@ pub async fn get_static_props(_path: String) -> Result() -> perseus::template::TemplateFn { - Box::new(|props: Option| { + Arc::new(|props: Option| { template! { IndexPage( serde_json::from_str::(&props.unwrap()).unwrap() diff --git a/examples/showcase/app/src/pages/ip.rs b/examples/showcase/app/src/pages/ip.rs index c6fe95c1c0..3f2ab7cdf7 100644 --- a/examples/showcase/app/src/pages/ip.rs +++ b/examples/showcase/app/src/pages/ip.rs @@ -5,6 +5,7 @@ use perseus::template::{Template}; use perseus::Request; use serde::{Deserialize, Serialize}; use sycamore::prelude::{component, template, GenericNode, Template as SycamoreTemplate}; +use std::sync::Arc; #[derive(Serialize, Deserialize)] pub struct IpPageProps { @@ -24,7 +25,7 @@ pub fn dashboard_page(props: IpPageProps) -> SycamoreTemplate { pub fn get_page() -> Template { Template::new("ip") - .request_state_fn(Box::new(get_request_state)) + .request_state_fn(Arc::new(get_request_state)) .template(template_fn()) } @@ -44,7 +45,7 @@ pub async fn get_request_state(_path: String, req: Request) -> Result() -> perseus::template::TemplateFn { - Box::new(|props: Option| { + Arc::new(|props: Option| { template! { IpPage( serde_json::from_str::(&props.unwrap()).unwrap() diff --git a/examples/showcase/app/src/pages/new_post.rs b/examples/showcase/app/src/pages/new_post.rs index 8ff0aec5d0..c367ff6bef 100644 --- a/examples/showcase/app/src/pages/new_post.rs +++ b/examples/showcase/app/src/pages/new_post.rs @@ -1,5 +1,6 @@ use perseus::template::Template; use sycamore::prelude::{component, template, GenericNode, Template as SycamoreTemplate}; +use std::sync::Arc; #[component(NewPostPage)] pub fn new_post_page() -> SycamoreTemplate { @@ -13,7 +14,7 @@ pub fn get_page() -> Template { } pub fn template_fn() -> perseus::template::TemplateFn { - Box::new(|_| { + Arc::new(|_| { template! { NewPostPage() } diff --git a/examples/showcase/app/src/pages/post.rs b/examples/showcase/app/src/pages/post.rs index d76b22b29d..d1d08331a8 100644 --- a/examples/showcase/app/src/pages/post.rs +++ b/examples/showcase/app/src/pages/post.rs @@ -2,6 +2,7 @@ use perseus::template::Template; use perseus::errors::ErrorCause; use serde::{Deserialize, Serialize}; use sycamore::prelude::{component, template, GenericNode, Template as SycamoreTemplate}; +use std::sync::Arc; #[derive(Serialize, Deserialize)] pub struct PostPageProps { @@ -25,8 +26,8 @@ pub fn post_page(props: PostPageProps) -> SycamoreTemplate { pub fn get_page() -> Template { Template::new("post") - .build_paths_fn(Box::new(get_static_paths)) - .build_state_fn(Box::new(get_static_props)) + .build_paths_fn(Arc::new(get_static_paths)) + .build_state_fn(Arc::new(get_static_props)) .incremental_path_rendering(true) .template(template_fn()) } @@ -58,7 +59,7 @@ pub async fn get_static_paths() -> Result, String> { } pub fn template_fn() -> perseus::template::TemplateFn { - Box::new(|props: Option| { + Arc::new(|props: Option| { template! { PostPage( serde_json::from_str::(&props.unwrap()).unwrap() diff --git a/examples/showcase/app/src/pages/time.rs b/examples/showcase/app/src/pages/time.rs index 1e36ed6967..25f8b3e823 100644 --- a/examples/showcase/app/src/pages/time.rs +++ b/examples/showcase/app/src/pages/time.rs @@ -2,6 +2,7 @@ use perseus::template::Template; use perseus::errors::ErrorCause; use serde::{Deserialize, Serialize}; use sycamore::prelude::{component, template, GenericNode, Template as SycamoreTemplate}; +use std::sync::Arc; #[derive(Serialize, Deserialize, Debug)] pub struct TimePageProps { @@ -21,8 +22,8 @@ pub fn get_page() -> Template { // This page will revalidate every five seconds (to illustrate revalidation) .revalidate_after("5s".to_string()) .incremental_path_rendering(true) - .build_state_fn(Box::new(get_build_state)) - .build_paths_fn(Box::new(get_build_paths)) + .build_state_fn(Arc::new(get_build_state)) + .build_paths_fn(Arc::new(get_build_paths)) } pub async fn get_build_state(_path: String) -> Result { @@ -37,7 +38,7 @@ pub async fn get_build_paths() -> Result, String> { } pub fn template_fn() -> perseus::template::TemplateFn { - Box::new(|props: Option| { + Arc::new(|props: Option| { template! { TimePage( serde_json::from_str::(&props.unwrap()).unwrap() diff --git a/examples/showcase/app/src/pages/time_root.rs b/examples/showcase/app/src/pages/time_root.rs index c04f7267af..e03de43058 100644 --- a/examples/showcase/app/src/pages/time_root.rs +++ b/examples/showcase/app/src/pages/time_root.rs @@ -2,6 +2,7 @@ use perseus::template::Template; use perseus::errors::ErrorCause; use serde::{Deserialize, Serialize}; use sycamore::prelude::{component, template, GenericNode, Template as SycamoreTemplate}; +use std::sync::Arc; #[derive(Serialize, Deserialize, Debug)] pub struct TimePageProps { @@ -21,8 +22,8 @@ pub fn get_page() -> Template { // This page will revalidate every five seconds (to illustrate revalidation) // Try changing this to a week, even though the below custom logic says to always revalidate, we'll only do it weekly .revalidate_after("5s".to_string()) - .should_revalidate_fn(Box::new(|| async { Ok(true) })) - .build_state_fn(Box::new(get_build_state)) + .should_revalidate_fn(Arc::new(|| async { Ok(true) })) + .build_state_fn(Arc::new(get_build_state)) } pub async fn get_build_state(_path: String) -> Result { @@ -33,7 +34,7 @@ pub async fn get_build_state(_path: String) -> Result() -> perseus::template::TemplateFn { - Box::new(|props: Option| { + Arc::new(|props: Option| { template! { TimePage( serde_json::from_str::(&props.unwrap()).unwrap() diff --git a/examples/showcase/bonnie.toml b/examples/showcase/bonnie.toml index d73258c360..1dae7d15fb 100644 --- a/examples/showcase/bonnie.toml +++ b/examples/showcase/bonnie.toml @@ -12,7 +12,8 @@ build.subcommands.--watch = [ "bonnie clean", "find ../../ -not -path \"../../target/*\" -not -path \"../../.git/*\" -not -path \"../../examples/showcase/app/dist/*\" | entr -s \"bonnie build\"" ] -serve = [ - "cd server", +serve.cmd = [ + "cd server-%server", "cargo watch -w ../../../ -x \"run\"" ] +serve.args = [ "server" ] \ No newline at end of file diff --git a/examples/showcase/server/Cargo.toml b/examples/showcase/server-actix-web/Cargo.toml similarity index 77% rename from examples/showcase/server/Cargo.toml rename to examples/showcase/server-actix-web/Cargo.toml index fa888481af..f2472c9b01 100644 --- a/examples/showcase/server/Cargo.toml +++ b/examples/showcase/server-actix-web/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "perseus-showcase-server" +name = "perseus-showcase-server-actix-web" version = "0.1.0" edition = "2018" @@ -7,6 +7,7 @@ edition = "2018" [dependencies] perseus = { path = "../../../packages/perseus" } +perseus-actix-web = { path = "../../../packages/perseus-actix-web" } actix-web = "3.3" actix-files = "0.5" urlencoding = "2.1" diff --git a/examples/showcase/server-actix-web/src/main.rs b/examples/showcase/server-actix-web/src/main.rs new file mode 100644 index 0000000000..0be571c6a9 --- /dev/null +++ b/examples/showcase/server-actix-web/src/main.rs @@ -0,0 +1,26 @@ +use perseus::config_manager::FsConfigManager; +use perseus_actix_web::{configurer, Options}; +use perseus_showcase_app::pages; +use sycamore::SsrNode; +use actix_web::{HttpServer, App}; + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .configure( + configurer( + Options { + index: "../app/index.html".to_string(), + js_bundle: "../app/pkg/bundle.js".to_string(), + wasm_bundle: "../app/pkg/perseus_showcase_app_bg.wasm".to_string(), + templates_map: pages::get_templates_map::() + }, + FsConfigManager::new() + ) + ) + }) + .bind(("localhost", 8080))? + .run() + .await +} \ No newline at end of file diff --git a/examples/showcase/server/src/main.rs b/examples/showcase/server/src/main.rs deleted file mode 100644 index 251b128176..0000000000 --- a/examples/showcase/server/src/main.rs +++ /dev/null @@ -1,67 +0,0 @@ -use actix_files::NamedFile; -use actix_web::{http::StatusCode, web, App, HttpRequest, HttpResponse, HttpServer}; -use std::collections::HashMap; -use sycamore::prelude::SsrNode; - -use perseus::{ - config_manager::FsConfigManager, - errors::err_to_status_code, - serve::{get_page, get_render_cfg}, - template::TemplateMap, -}; -use perseus_showcase_app::pages; - -mod conv_req; -use conv_req::convert_req; - -#[actix_web::main] -async fn main() -> std::io::Result<()> { - HttpServer::new(|| { - App::new() - .data(get_render_cfg().expect("Couldn't get render configuration!")) - .data(FsConfigManager::new()) - .data(pages::get_templates_map::()) - // TODO chunk JS and WASM bundles - // These allow getting the basic app code (not including the static data) - // This contains everything in the spirit of a pseudo-SPA - .route("/.perseus/bundle.js", web::get().to(js_bundle)) - .route("/.perseus/bundle.wasm", web::get().to(wasm_bundle)) - // This allows getting the static HTML/JSON of a page - // We stream both together in a single JSON object so SSR works (otherwise we'd have request IDs and weird caching...) - .route("/.perseus/page/{filename:.*}", web::get().to(page_data)) - // For everything else, we'll serve the app shell directly - .default_service(web::route().to(initial)) - }) - .bind(("localhost", 8080))? - .run() - .await -} - -async fn initial() -> std::io::Result { - NamedFile::open("../app/index.html") -} -async fn js_bundle() -> std::io::Result { - NamedFile::open("../app/pkg/bundle.js") -} -async fn wasm_bundle() -> std::io::Result { - NamedFile::open("../app/pkg/perseus_showcase_app_bg.wasm") -} -async fn page_data( - req: HttpRequest, - templates: web::Data>, - render_cfg: web::Data>, - config_manager: web::Data, -) -> HttpResponse { - let path = req.match_info().query("filename"); - // We need to turn the Actix Web request into one acceptable for Perseus (uses `http` internally) - // TODO proper error handling here - let http_req = convert_req(&req).unwrap(); - let page_data = get_page(path, http_req, &render_cfg, &templates, config_manager.get_ref()).await; - - match page_data { - Ok(page_data) => HttpResponse::Ok().body(serde_json::to_string(&page_data).unwrap()), - // We parse the error to return an appropriate status code - Err(err) => HttpResponse::build(StatusCode::from_u16(err_to_status_code(&err)).unwrap()) - .body(err.to_string()), - } -} diff --git a/packages/perseus-actix-web/Cargo.toml b/packages/perseus-actix-web/Cargo.toml new file mode 100644 index 0000000000..1b83b55999 --- /dev/null +++ b/packages/perseus-actix-web/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "perseus-actix-web" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +perseus = { path = "../perseus" } +actix-web = "3.3" +actix-files = "0.5" +urlencoding = "2.1" +serde_json = "1" +error-chain = "0.12" +futures = "0.3" +sycamore = { version = "0.5.1", features = ["ssr"] } \ No newline at end of file diff --git a/packages/perseus-actix-web/src/configurer.rs b/packages/perseus-actix-web/src/configurer.rs new file mode 100644 index 0000000000..dc9896e773 --- /dev/null +++ b/packages/perseus-actix-web/src/configurer.rs @@ -0,0 +1,55 @@ +use actix_files::NamedFile; +use actix_web::web; +use sycamore::prelude::SsrNode; + +use perseus::{ + config_manager::ConfigManager, + serve::get_render_cfg, + template::TemplateMap, +}; + +use crate::page_data::page_data; + +/// The options for setting up the Actix Web integration. This should be literally constructed, as nothing is optional. +#[derive(Clone)] +pub struct Options { + /// The location on the filesystem of your JavaScript bundle. + pub js_bundle: String, + /// The location on the filesystem of your WASM bundle. + pub wasm_bundle: String, + /// The location on the filesystem of your `index.html` file that includes the JS bundle. + pub index: String, + /// A `HashMap` of your app's templates by their paths. + pub templates_map: TemplateMap +} + +async fn js_bundle(opts: web::Data) -> std::io::Result { + NamedFile::open(&opts.js_bundle) +} +async fn wasm_bundle(opts: web::Data) -> std::io::Result { + NamedFile::open(&opts.wasm_bundle) +} +async fn index(opts: web::Data) -> std::io::Result { + NamedFile::open(&opts.index) +} + +/// Configures an existing Actix Web app for Perseus. This returns a function that does the configuring so it can take arguments. +pub fn configurer(opts: Options, config_manager: C) -> impl Fn(&mut web::ServiceConfig) { + move |cfg: &mut web::ServiceConfig| { + cfg + .data(get_render_cfg().expect("Couldn't get render configuration!")) + .data(config_manager.clone()) + .data(opts.clone()) + // TODO chunk JS and WASM bundles + // These allow getting the basic app code (not including the static data) + // This contains everything in the spirit of a pseudo-SPA + .route("/.perseus/bundle.js", web::get().to(js_bundle)) + .route("/.perseus/bundle.wasm", web::get().to(wasm_bundle)) + // This allows getting the static HTML/JSON of a page + // We stream both together in a single JSON object so SSR works (otherwise we'd have request IDs and weird caching...) + .route("/.perseus/page/{filename:.*}", web::get().to(page_data::)) + // For everything else, we'll serve the app shell directly + // FIXME + .route("*", web::get().to(index)); + } +} \ No newline at end of file diff --git a/examples/showcase/server/src/conv_req.rs b/packages/perseus-actix-web/src/conv_req.rs similarity index 79% rename from examples/showcase/server/src/conv_req.rs rename to packages/perseus-actix-web/src/conv_req.rs index 2e65349e18..b48acd256c 100644 --- a/examples/showcase/server/src/conv_req.rs +++ b/packages/perseus-actix-web/src/conv_req.rs @@ -1,8 +1,8 @@ use perseus::{HttpRequest, Request}; +use crate::errors::*; -// TODO set up proper error handling in an integration crate /// Converts an Actix Web request into an `http::request`. -pub fn convert_req(raw: &actix_web::HttpRequest) -> Result { +pub fn convert_req(raw: &actix_web::HttpRequest) -> Result { let mut builder = HttpRequest::builder(); // Add headers one by one for (name, val) in raw.headers() { @@ -16,11 +16,9 @@ pub fn convert_req(raw: &actix_web::HttpRequest) -> Result { // The HTTP version used builder = builder.version(raw.version()); - let req = builder + builder // We always use an empty body because, in a Perseus request, only the URI matters // Any custom data should therefore be sent in headers (if you're doing that, consider a dedicated API) .body(()) - .map_err(|err| format!("converting actix web request to perseus-compliant request failed: '{}'", err))?; - - Ok(req) + .map_err(|err| ErrorKind::RequestConversionFailed(err.to_string()).into()) } \ No newline at end of file diff --git a/packages/perseus-actix-web/src/errors.rs b/packages/perseus-actix-web/src/errors.rs new file mode 100644 index 0000000000..e618d745a2 --- /dev/null +++ b/packages/perseus-actix-web/src/errors.rs @@ -0,0 +1,29 @@ +#![allow(missing_docs)] + +pub use error_chain::bail; +use error_chain::error_chain; + +// The `error_chain` setup for the whole crate +error_chain! { + // The custom errors for this crate (very broad) + errors { + /// + JsErr(err: String) { + description("an error occurred while interfacing with javascript") + display("the following error occurred while interfacing with javascript: {:?}", err) + } + /// For if converting an HTTP request from Actix Web format to Perseus format failed. + RequestConversionFailed(err: String) { + description("converting the request from actix-web format to perseus format failed") + display("converting the request from actix-web format to perseus format failed: {:?}", err) + } + } + links { + ConfigManager(::perseus::config_manager::Error, ::perseus::config_manager::ErrorKind); + } + // We work with many external libraries, all of which have their own errors + foreign_links { + Io(::std::io::Error); + Json(::serde_json::Error); + } +} \ No newline at end of file diff --git a/packages/perseus-actix-web/src/lib.rs b/packages/perseus-actix-web/src/lib.rs new file mode 100644 index 0000000000..eb5140eea8 --- /dev/null +++ b/packages/perseus-actix-web/src/lib.rs @@ -0,0 +1,6 @@ +mod conv_req; +pub mod errors; +mod page_data; +mod configurer; + +pub use crate::configurer::{configurer, Options}; \ No newline at end of file diff --git a/packages/perseus-actix-web/src/page_data.rs b/packages/perseus-actix-web/src/page_data.rs new file mode 100644 index 0000000000..ad61fde0de --- /dev/null +++ b/packages/perseus-actix-web/src/page_data.rs @@ -0,0 +1,36 @@ +use perseus::{ + config_manager::ConfigManager, + errors::err_to_status_code, + serve::get_page, +}; +use actix_web::{web, HttpRequest, HttpResponse, http::StatusCode}; +use std::collections::HashMap; +use crate::conv_req::convert_req; +use crate::Options; + +/// The handler for calls to `.perseus/page/*`. This will manage returning errors and the like. +pub async fn page_data( + req: HttpRequest, + opts: web::Data, + render_cfg: web::Data>, + config_manager: web::Data, +) -> HttpResponse { + let templates = &opts.templates_map; + let path = req.match_info().query("filename"); + // We need to turn the Actix Web request into one acceptable for Perseus (uses `http` internally) + let http_req = convert_req(&req); + let http_req = match http_req { + Ok(http_req) => http_req, + // If this fails, the client request is malformed, so it's a 400 + Err(err) => return HttpResponse::build(StatusCode::from_u16(400).unwrap()) + .body(err.to_string()) + }; + let page_data = get_page(path, http_req, &render_cfg, templates, config_manager.get_ref()).await; + + match page_data { + Ok(page_data) => HttpResponse::Ok().body(serde_json::to_string(&page_data).unwrap()), + // We parse the error to return an appropriate status code + Err(err) => HttpResponse::build(StatusCode::from_u16(err_to_status_code(&err)).unwrap()) + .body(err.to_string()), + } +} \ No newline at end of file diff --git a/packages/perseus/src/config_manager.rs b/packages/perseus/src/config_manager.rs index 77deaeb8ee..92a357d376 100644 --- a/packages/perseus/src/config_manager.rs +++ b/packages/perseus/src/config_manager.rs @@ -29,14 +29,14 @@ error_chain! { // TODO make config managers asynchronous /// A trait for systems that manage where to put configuration files. At simplest, we'll just write them to static files, but they're /// more likely to be stored on a CMS. -pub trait ConfigManager { +pub trait ConfigManager: Clone { /// Reads data from the named asset. fn read(&self, name: &str) -> Result; /// Writes data to the named asset. This will create a new asset if one doesn't exist already. fn write(&self, name: &str, content: &str) -> Result<()>; } -#[derive(Default)] +#[derive(Default, Clone)] pub struct FsConfigManager {} impl FsConfigManager { /// Creates a new filesystem configuration manager. This function only exists to preserve the API surface of the trait. diff --git a/packages/perseus/src/template.rs b/packages/perseus/src/template.rs index f72f48f045..76de1b1460 100644 --- a/packages/perseus/src/template.rs +++ b/packages/perseus/src/template.rs @@ -6,6 +6,7 @@ use futures::Future; use std::collections::HashMap; use std::pin::Pin; use sycamore::prelude::{GenericNode, Template as SycamoreTemplate}; +use std::sync::Arc; /// Represents all the different states that can be generated for a single template, allowing amalgamation logic to be run with the knowledge /// of what did what (rather than blindly working on a vector). @@ -98,17 +99,18 @@ make_async_trait!( make_async_trait!(ShouldRevalidateFnType, StringResultWithCause); // A series of closure types that should not be typed out more than once -pub type TemplateFn = Box) -> SycamoreTemplate>; -pub type GetBuildPathsFn = Box; -pub type GetBuildStateFn = Box; -pub type GetRequestStateFn = Box; -pub type ShouldRevalidateFn = Box; -pub type AmalgamateStatesFn = Box StringResultWithCause>>; +pub type TemplateFn = Arc) -> SycamoreTemplate>; +pub type GetBuildPathsFn = Arc; +pub type GetBuildStateFn = Arc; +pub type GetRequestStateFn = Arc; +pub type ShouldRevalidateFn = Arc; +pub type AmalgamateStatesFn = Arc StringResultWithCause>>; /// This allows the specification of all the template templates in an app and how to render them. If no rendering logic is provided at all, /// the template will be prerendered at build-time with no state. All closures are stored on the heap to avoid hellish lifetime specification. /// All properties for templates are passed around as strings to avoid type maps and other horrible things, this only adds one extra /// deserialization call at build time. +#[derive(Clone)] pub struct Template { /// The path to the root of the template. Any build paths will be inserted under this. path: String, @@ -150,7 +152,7 @@ impl Template { pub fn new(path: impl Into + std::fmt::Display) -> Self { Self { path: path.to_string(), - template: Box::new(|_: Option| sycamore::template! {}), + template: Arc::new(|_: Option| sycamore::template! {}), get_build_paths: None, incremental_path_rendering: false, get_build_state: None,