Replaced code generation with templates

This commit is contained in:
Mutzi 2023-10-01 13:15:12 +02:00
parent 2c1e684e33
commit 3ca0893a94
Signed by: root
GPG Key ID: 2437494E09F13876
27 changed files with 4175 additions and 505 deletions

3
.gitignore vendored
View File

@ -131,3 +131,6 @@ Cargo.lock
*.pdb *.pdb
# End of https://www.toptal.com/developers/gitignore/api/rust,clion # End of https://www.toptal.com/developers/gitignore/api/rust,clion
run/
src/templates/

View File

@ -3,6 +3,9 @@ name = "mrpc"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
[build-dependencies]
ructe = { path = "ructe-0.17.0" }
[dependencies] [dependencies]
codespan-reporting = "0.11.1" codespan-reporting = "0.11.1"
once_cell = "1.18.0" once_cell = "1.18.0"

3
build.rs Normal file
View File

@ -0,0 +1,3 @@
fn main() -> ructe::Result<()> {
ructe::Ructe::new("src/templates".into())?.compile_templates("templates")
}

4
ructe-0.17.0/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
*~
target
Cargo.lock

556
ructe-0.17.0/CHANGELOG.md Normal file
View File

@ -0,0 +1,556 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this
project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Release 0.17.0 -- 2023-07-22
* Added a check that no more than one of the http-types, mime02, or
mime03 features are enabled (PR #124). Thanks @rustafarian-dev.
* Changed the writer type from `W: &mut Write` to just `W: Write` (PR #125).
Thanks @kornelski!
* Fixed handling of `MULTI_WORD_CONSTANTS` in templates (Issue #129, PR #130).
Thanks @wezm!
* More ways to create a working rust symbol name from a "strange"
static file name. Illegal characters are replaced by `_`, and if
the file name starts with a number it is prefixed with `n` (Issue
#82, PR #132). Thanks @Aedius for reporting!
* Fixed more clippy lints (PR #123, #127). Thanks @vbrandl!
* Updated `rsass` to 0.28.0 and `itertools` to 0.11.0.
## Release 0.16.1 -- 2023-01-28
* Msrv is 1.58.1, so let ructe itself use rust edition 2021.
* Use format strings with inline captures (in ructe itself and
generated code).
## Release 0.16.0 -- 2023-01-22
* Removed backwards compatible aliases for template functions.
In ructe 0.7.2 and earlier, a template file `page.rs.html` resulted
in a rust function `templates::page(...)`.
In 0.7.2, that was changed to `templates::page_html(...)` and the
old name was kept as a deprecated alias.
However, since the template functions are usually defined within the
same crate that defines them, the deprecation warning has usually
not been shown, and this removal may still be a surprise to some
users (it was even used in examples up to this change).
* Allow more lifetime arguments to templates in template arguments (PR
#122, fixes #121). Thanks to @wezm!
* Added axum example (PR #118). Thanks to @vbrandl!
* Updated rsass to 0.27.0 and base64 to 0.21.0.
* Updated dependencies in examples: actix-web 4.2.1, axum 0.6.2,
env_logger 0.10.0,
* Dropped support for rust edition 2015 in crates that directly uses
ructe.
## Releaase 0.15.0
* Breaking change: Most methods of `StaticFiles` now supports method
chaining, by returning `Result<&mut Self>`, making typical build scripts
nicer (PR #115).
* Update (optional) rsass to 0.26 (PR #116).
* Some doc improvements.
## Release 0.14.2 - 2022-08-20
* Improve error reporting. The debug output for `RucteError` is now the
same as display, and the standard `Error::source` is implemented.
* Fix clippy lint clippy::get-first (PR #114).
* Update optional rsass to 0.25.0.
Thanks to @vbrandl for PR #114.
## Release 0.14.0 - 2022-02-06
* Breaking change: The generated template functions have a simpler
signature.
* Allow litetimes in template argument types. Issue #106, PR #110.
* Improve error handling in optional warp support, PR #109.
* Current stable rust is 1.57, MSRV is now 1.46.0.
* Update nom dependency to 7.1.0.
* Update optional rsass to 0.23.0.
* Update env_logger to 0.9 and gotham to 0.7.1 in examples
* Dropped support for warp 0.2 (the warp02 feature and example).
Thanks to @JojiiOfficial for reporting #106.
## Release 0.13.4 - 2021-06-25
* Allow `else if` after an `@if` block in templates. PR #104, fixes #81.
* Add a missing `}` in doc example. PR #102.
* Update optional rsass to 0.22.0.
* Updated gotham example to 0.6.0.
Thanks @bearfrieze for #102 and @Aunmag for #81.
Tested with rustc 1.53.0, 1.48.0, 1.46.0, 1.44.1, 1.54.0-beta.1,
and 1.55.0-nightly (7c3872e6b 2021-06-24).
## Release 0.13.2 - 2021-03-14
* Improve formatting of README, PR #100.
* Update nom to 6.1.0, which raises the MSRV to 0.44
* Update base64 to 0.13 and itertools to 0.10.
* Update optional rsass to 0.19.0.
* Add warp 0.3 feature and example.
* Add tide 0.16 feaure and update example.
* Testing is now done with github actions rather than Travis CI.
* Minor clippy fixes, PR #99.
Thanks to @ibraheemdev for PR #100.
Tested with rustc 1.50.0 (cb75ad5db 2021-02-10),
1.48.0 (7eac88abb 2020-11-16),
1.46.0 (04488afe3 2020-08-24),
1.44.1 (c7087fe00 2020-06-17),
1.51.0-beta.6 (6a1835ad7 2021-03-12),
1.52.0-nightly (acca81892 2021-03-13)
## Release 0.13.0 - 2020-11-15
* Try to improve incremental compile times of projects using ructe by
only writing fils if their contents actually changed. Also some code
cleanup. PR #97.
* Update ructe itself to use edition 2018 (it is still useable for
projects using both editios). PR #98.
* Fix `StaticFiles::add_files_as` for empty `to` argument and add some
more documentation for it. Fixes issue #96.
* Update optional rsass dependency to 0.16.0.
* Add optional support for tide 0.14 and 0.15.
* Update gotham to 0.5 and axtix-web to 3.2 in examples.
Tested with rustc 1.47.0 (18bf6b4f0 2020-10-07),
1.42.0 (b8cedc004 2020-03-09), 1.40.0 (73528e339 2019-12-16),
1.48.0-beta.8 (121901459 2020-11-08), and
1.50.0-nightly (98d66340d 2020-11-14)
## Release 0.12.0 - 2020-08-14
* PR #80 and #94: Support Tide framework by a feature and an example.
* PR #91: Update basic examples to edition 2018.
* Issue #68, PR #90: Don't eat whitespace after a for loop.
* Issue #66, PR #89: Fix parse error for nested braces in expressions.
* PR #84: Use std::ascii::escape_default.
* PR #87: Provide ToHtml::to_buffer()
* Forbid unsafe and undocumented code.
* The build is on https://travis-ci.com/kaj/ructe now.
* Internal cleanup.
## Release 0.11.4 - 2020-04-25
* Improve `@match` parsing.
## Release 0.11.2 - 2020-04-22
* Bugfix: Allow space before laste brace in `@match`.
## Release 0.11.0 - 2020-04-21
* PR #73, Issue #38: Add support for `@match` statements.
Thanks to @vivlim for the issue.
## Release 0.10.0 - 2020-04-19
* Update rsass to 0.13.0 and improve sass error handling.
* Drop the warp01 feature.
* PR #72 from @kornelski: Avoid clobbering variable name.
* Update itertools to 0.9.0 and base64 to 0.12.0.
Thanks to @kornelski for suggestions and bug reports.
## Release 0.9.2 - 2020-01-25
* PR #70, Issue #63: Add feature warp02, supportig warp 0.2.x, and add
a name alias warp01 for the old warp 0.1.x feature. Same in
examples.
* PR #69, Issue #67: Anyting that is allowed in a string in Rust
should be allowed in a string in ructe.
* Fix clippy complaints re statics in generated code.
* Update actix-web example to 2.0.
* Fix doctest with mime03 feature.
Thanks to @nocduro and @Aunmag for suggestions and bug reports.
## Release 0.9.0 - 2019-12-25
* PR #65, Issue #64: An expression starting with paren ends on close.
BREAKING: Before this change, calling a function on the result of
some subexpression could be written as `@(a - b).abs()`. After this
change, that should be changed to `@((a - b).abs())` unless the
intent is to have the result of (a - b) followed by the template
string `.abs()`.
* RucteError now implements std::error::Error.
* Specify which references in examples are `dyn` or `impl`.
* Remove a useless string clone.
* Update rsass to 0.12.0.
Thanks to @Aunmag.
## Release 0.8.0 - 2019-11-06
* Issue #62: New version number due to a semver-breaking change,
reported by @kornelski.
Otherwise same as 0.7.4:
* PR #55 from kornelski: Improve benchmarks.
* Part of issue #20: Allow template source files to be named *.rs.svg
or *.rs.xml as well as *.rs.html. The generated template functions
will simlilarly be suffixed _svg, _xml or _html (any template_html
will get a template alias, for backwards compatibility.
* PR #61 from Eroc33: Improve parsing for tuple and generic type
expressions.
* Fix old doc link in readme.
* Update dependencies in ructe and examples.
Thaks to @kornelski and @Eroc33.
## Redacted: Relase 0.7.4 - 2019-11-02
* PR #55 from kornelski: Improve benchmarks.
* Part of issue #20: Allow template source files to be named
`*.rs.svg` or `*.rs.xml` as well as `*.rs.html`. The generated
template functions will simlilarly be suffixed `_svg`, `_xml` or
`_html` (any `template_html` will get a `template` alias, for
backwards compatibility.
* PR #61 from @Eroc33: Improve parsing for tuple and generic type
expressions.
* Fix old doc link in readme.
* Update dependencies in ructe and examples.
Thaks to @kornelski and @Eroc33.
## Release 0.7.2 - 2019-08-28
* Issue #53, PR #60: Allow empty strings everywhere quoted strings are
allowed.
* Issue #57, PR #59: Accept explicit impl and dyn in types.
* Relax over-strict whitespace requirements, fix a regression in 0.7.0.
* PR #56: Require buf reference to implement Write, not buf itself
* PR #58: Fix warnings in generated code.
* Remove no-longer-used imports.
Thanks to @kornelski for multiple contributions.
## Release 0.7.0 - 2019-07-18
* PR #52: Upgrade nom to 5.0
* Update rsass to 0.11.0 (which also uses nom 5.0)
* Improve template declaration parsing and diagnostics.
* PR #50 and #51 from @dkotrada: Fix typos in actix example.
* Remove deprecated functions.
## Release 0.6.4 - 2019-06-23
* Added more modern rust compiler versions (and dropped 1.26).
* PR #49: Add an actix example.
* PR #48 from @Noughmad: Use `impl Write` or generic argument instead
of dynamic traits. Fixes a warning for each template when using
edition 2018 in nightly rust.
* Clearer doc about escaping special characters.
* PR #46 from @kornelski: Add missing crates keyword
## Release 0.6.2 - 2019-03-16
* Improved documentation and examples.
All public items now have documentation.
* Improve build-time error handling.
If there is an error involving an environment variable, include the
variable name in the message.
* Take more Path build-time arguements AsRef.
Make it possible to simpl use a string literal as a path in more places.
## Release 0.6.0 - 2019-03-14
* PR #45: Provide a warp feature.
All my warp + ructe projects use the same RenderRucte extension trait
to make calling the templates on generating responses a bit clearer.
Provide that trait here as an optional feature.
* PR #43: Make the build scripts nicer.
Provide a struct Ructe with methods to handle the red tape from build
scripts. Make the remaining parts of the build scripts shorter and
more to the point.
* Use edition 2018 in warp example.
* Fix examples lang attribute.
A whole bunch of examples had the html lang attibute set to sv when
the content is actually in English.
## Release 0.5.10 - 2019-02-22
* Convert more file names to rust names (a file name might contain
dashes and dots that needs to be converted to something else
(underscore) to work in a rust name).
* Find new files in static dirs (add a cargo:rerun-if-changed line
for the directory itself).
## Release 0.5.8 - 2019-02-16
* Adapt to rsass 0.9.8 (the sass feature now requires a compiler that
supports edition 2018).
* More compact static data, using byte strings instead of numbers.
(i.e. b"\xef\xbb\xbfabc" rather than [239, 187, 191, 65, 66, 67]).
* Minor internal cleanup.
* Update bytecount dependency.
## Release 0.5.6 - 2019-01-05
* PR #41: Benchmark and improve performance of html-escaping.
* PR #39: Silence a clippy warning about old syntax in silencing
another warning.
* Update itertools to 0.8 (and env_logger in warp example)
Thanks to @kornelski for PRs #39 and #41.
## Release 0.5.4 - 2018-11-30
* Support struct unpacking in `@if` and `@for` expressions.
## Release 0.5.2 - 2018-11-04
* Special case for empty sub-templates, mainly to avoid a warning when
compiling generated code.
* Update md5 to 0.6.
* Update gotham in example to 0.3.0.
* Use mime 0.3 in static example, and remove mime03 example.
## Release 0.5.0 - 2018-11-03
* Support multiple Content arguments.
Impl Trait is used to make sub-templates as arguments less magic.
This way we can also support more than one Content argument to the
same template.
* PR #36 / Issue #35: Test and fix support for edition=2018.
Module paths used by generated code are now compatible with the 2018
edition. Also, some code in examples and documentation use more
2018-friendly module paths.
* PR 34: Use bytecount rather than simple counting, elide one lifetime.
* Update nom to 4.1.1, base64 to 0.10.0, bytecount to 0.4, and md5 to 0.5.
* Update iron to 0.6 and warp to 0.1.9 in examples.
* Minor cleanup in nickel example.
Thanks to @KlossPeter for PR #34 and @matthewpflueger for issue #35.
## Release 0.4.6 - 2018-10-07
* Lock nom version at 4.0, since it seems the 4.1 release is
incompatible with the error handling in ructe.
## Release 0.4.4 - 2018-09-06
* Test and fix #33, unduplicate curly brackets.
* Add `@@` escape, producing a single `@` sign. Suggested in #33.
* Some more mime types for static files.
* Update dependencies: nom 4.0, rsass 0.9.0
* Add a warp example, and link to kaj/warp-diesel-ructe-sample
Thanks to @dermetfan for reporting issue #33.
## Release 0.4.2 - 2018-08-01
* Test and fix issue #31, comments in body.
Thanks to @jo-so for reporting the issue, and for the test
## Release 0.4.0 - 2018-07-05
* Template syntax:
- Allow local ranges (i.e. `2..7`) in loop expressions.
- Allow underscore rust names. There is use for unused variables in
templates, so allow names starting with underscore.
- Issue #24 / PR #28: Allow logic operators in `@if ...` expressions.
- Issue #25 / PR #27: Allow much more in parentehsis expressions.
* Improved examples:
- A new design for the framework examples web page, using svg graphics.
- Improve code and inline documentation of iron and nickel examples.
- Add a similar example with the Gotham framework.
* Recognize `.svg` static files.
* Allocate much fewer strings when parsing expressions in templates.
* PR #26: use `write_all` rather than the `write!` macro in generated
code, contributed by @kornelski
* Fix `application/octet-stream` MIME type. Contributed by @kornelski.
* Use `write_str`/`write_all` when generating output. Contributed by
@kornelski.
## Release 0.3.16 - 2018-04-08
Changes since 0.3.14 is mainly some internal cleanup, a link fix in
README and the optional rsass dependency is updated to 0.8.0.
## Release 0.3.14 - 2018-03-11
* Make the space after a comma in list expressions optional.
* Allow enum variants (and module names) in expressions.
* Some cleanup in parser code.
## Release 0.3.12 - 2018-02-10
* Add a way to add static files without hashnames.
A static file can be added and mapped as an arbitrary name, or a
directory can be recursively added with an arbitrary prefix.
## Release 0.3.10 - 2017-12-30
* Allow `*` at start of expressions (and subexpressions).
* Updated (optional) rsass to ^0.7.0.
* Updated base64 to ^0.9.0.
## Release 0.3.8 - 2017-12-07
* Make clippy happy with the code genarated for templates.
* Updated lazy_static to 1.0.
* Updated base64 to 0.8.
* Updated (optional) rsass to 0.6.
## Relese 0.3.6 - 2017-11-05
* Update nom dependency to version 3.2.
* Update optional rsass dependency to version 0.5.0.
* Update base64 dependency to 0.7.0.
* A documentation typo fixed, by @jo-so.
* Minor internal cleanup.
## Release 0.3.4 - 2017-07-10
* PR #15, issue #14: Allow destructure in loops, thanks to @nubis.
* PR #16 Allow complex argument types to templates
* PR #17 Write a doc chapter about static content.
## Release 0.3.2 - 2017-06-23
* Fix a bug in ordering (and therefor findability) of static files.
* Improved documentation.
* PR #13: Provide mime type for static file data.
* Fix file paths for `@import` in scss (using sass feature).
* Code cleanup (use `?` operator rather than `try!` macro).
* Use include_bytes for static files to improve compile times.
## Release 0.3.0 - 2017-05-07
* Issue #10: Watch template directories for changes, to build new
templates when they are created.
* PR #12: Integrate sass, including a function to reference static
files from a scss document.
* Some documentation improvements and internal code cleanup.
## Release 0.2.6 - 2017-03-23
* #8 / PR #11: Much improved error reporting.
* #9 allow comparison operators in if statements.
* Improved documentation, including a chapter on template structure in
the docs (based on what was in the README.md).
This release is tested with rust versions 1.14.0, 1.15.1, 1.16.0
(stable), 1.17.0-beta.2 (b7c276653 2017-03-20), and 1.17.0-nightly
(8c4f2c64c 2017-03-22).
## Release 0.2.4 - 2017-02-05
* Test: expression may be in string, such as `<a href="@foo">...</a>`.
This should work for all expressions.
* Handle escaped quotes in strings.
* PR #7: Allow slices in templates.
* Stop using the nom macro chain!, which is deprecated.
This release is tested with rust versions 1.14.0, 1.15.0 (stable),
1.16.0-beta.1 (beta), and 1.17.0-nightly (0648517fa 2017-02-03).
## Release 0.2.2 - 2017-01-29
* PR #3: Add convenient handling of static files
* Add documentation for utilities.
## Release 0.2.0 - 2017-01-28
* PR #6, Issue #5: Template directory structure. Finds templates in
all subdirectories of the template dir rather than only the template
dir itself. Template functions are created in a module structure
that mirrors the directory structure.
Thanks to @mrLSD for suggestion.
* Update `nom` to 2.0
* Use `base64` instead of entire `rustc_serialize` for just base64 coding.
* Issue #4: Mention curly brackets escaping in docs. Thanks to @dermetfan.
* Cleanup, more tests and documentation.
## Release 0.1.2 - 2016-11-20
* Allow expressions to start with boolean not.
* DRYer test code.
* Write the generated code for each template into a separate file.
As suggested by Jethro Beekman in
[a comment on my blog](https://rasmus.krats.se/2016/ructe.en#c2427).
* More calling templates from templates doc.
## Release 0.1.1 - 2016-10-03
* Support calling templates with body arguments. Usefull for
"base-page" templates.
* Provide `Html` trait to template code.
* Some testing and cleanup.
## Version 0.1.0 - 2016-09-24
* First version published on crates.io.
* Support for `@if` and `@for` blocks.
* Improved expression parsing with chaining, square and curly brakets,
and string literals.
* Compile all found templates.
* More tests and documentation.
## Initial commit - 2016-09-14
Very siple templates, with arguments, worked, and text was
html-escaped as needed.

38
ructe-0.17.0/Cargo.toml Normal file
View File

@ -0,0 +1,38 @@
[package]
name = "ructe"
version = "0.17.0"
authors = ["Rasmus Kaj <kaj@kth.se>"]
description = "Rust Compiled Templates, efficient type-safe web page templates."
documentation = "https://docs.rs/ructe"
repository = "https://github.com/kaj/ructe"
readme = "README.md"
keywords = ["web", "templating", "template", "html"]
categories = ["template-engine", "web-programming"]
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.58.1"
[features]
sass = ["rsass"]
mime02 = []
mime03 = ["mime"]
warp03 = ["mime03"]
http-types = []
tide016 = ["tide013"]
tide015 = ["tide013"]
tide014 = ["tide013"]
tide013 = ["http-types"]
[dependencies]
base64 = "0.21.0"
bytecount = "0.6.0"
itertools = "0.11.0"
md5 = "0.7"
nom = "7.1.0"
rsass = { version = "0.28.0", optional = true }
mime = { version = "0.3", optional = true }
[badges]
travis-ci = { repository = "kaj/ructe" }
maintenance = { status = "actively-developed" }

78
ructe-0.17.0/README.md Normal file
View File

@ -0,0 +1,78 @@
# Rust Compiled Templates — ructe
This is my attempt at writing a HTML template system for Rust.
Some inspiration comes from the scala template system used in play 2,
as well as plain old jsp.
[![Crate](https://img.shields.io/crates/v/ructe.svg)](https://crates.io/crates/ructe)
[![docs](https://docs.rs/ructe/badge.svg)](https://docs.rs/ructe)
[![CI](https://github.com/kaj/ructe/workflows/CI/badge.svg)](https://github.com/kaj/ructe/actions)
## Design criteria
* As many errors as possible should be caught at compile-time.
* A compiled binary should include all the template code it needs,
no need to read template files at runtime.
* Compilation may take time, running should be fast.
* Writing templates should be almost as easy as writing html.
* The template language should be as expressive as possible.
* It should be possible to write templates for any text-like format,
not only html.
* Any value that implements the `Display` trait should be outputable.
* By default, all values should be html-escaped. There should be an
easy but explicit way to output preformatted html.
## Current status
Ructes is in a rather early stage, but does work;
templates can be transpiled to rust functions, which are then compiled
and can be called from rust code.
### Template format
A template consists of three basic parts:
First a preamble of `use` statements, each prepended by an `@` sign.
Secondly a declaration of the parameters the template takes.
And third, the template body.
The full syntax is described [in the documentation](https://docs.rs/ructe/).
Some examples can be seen in
[examples/simple/templates](examples/simple/templates).
A template may look something like this:
```html
@use any::rust::Type;
@use super::statics::style_css;
@(name: &str, items: &[Type])
<html>
<head>
<title>@name</title>
<link rel="stylesheet" href="/static/@style_css.name" type="text/css"/>
</head>
<body>
<h1>@name</h1>
<dl>
@for item in items {
<dt>@item.title()</dt>
<dd>@item.description()</dd>
}
</dl>
<body>
</html>
```
## How to use ructe
Ructe compiles your templates to rust code that should be compiled with
your other rust code, so it needs to be called before compiling,
as described [in the documentation](https://docs.rs/ructe/).
There are also [examples](examples),
both for ructe itself and its futures and for using it with the web
frameworks [actix-web](examples/actix), [gotham](examples/gotham),
[iron](examples/iron), [nickel](examples/nickel), [tide](examples/tide),
and [warp](examples/warp03).
There is also [a separate example of using ructe with warp and
diesel](https://github.com/kaj/warp-diesel-ructe-sample).

View File

@ -0,0 +1,3 @@
max_width = 78
reorder_imports = true

View File

@ -0,0 +1,304 @@
// This module is only a chapter of the documentation.
//! This module describes the template syntax used by ructe.
//!
//! The syntax is inspired by
//! [Twirl](https://github.com/playframework/twirl), the Scala-based
//! template engine in
//! [Play framework](https://www.playframework.com/),
//! but of course with rust types expressions instead of scala.
//!
//! A template consists of three basic parts:
//! First a preamble of `use` statements, each prepended by an @ sign.
//! Secondly a declaration of the parameters the template takes.
//! And third, the template body.
//!
//! ```html
//! @(name: &str, value: &u32)
//!
//! <html>
//! <head><title>@name</title></head>
//! <body>
//! <p>The value of @name is @value.</p>
//! <body>
//! </html>
//! ```
//!
//! As seen above, string slices and integers can easily be outputed
//! in the template body, using `@name` where `name` is a parameter of
//! the template.
//! Actually, more complex expressions can be outputed in the same
//! way, as long as the resulting value implements [`ToHtml`].
//! Rust types that implements [`Display`] automatically implements
//! [`ToHtml`] in such a way that contents are safely escaped for
//! html.
//!
//! ```html
//! @use any::rust::Type;
//!
//! @(name: &str, items: &[Type])
//!
//! <html>
//! <head><title>@name</title></head>
//! <body>
//! @if items.is_empty() {
//! <p>There are no items.</p>
//! } else {
//! <p>There are @items.len() items.</p>
//! <ul>
//! @for item in items {
//! <li>@item</li>
//! }
//! </ul>
//! <body>
//! </html>
//! ```
//!
//! The curly brackets, `{` and `}`, is used for blocks (see Loops,
//! Conditionals, and Calling other templates below).
//!
//! To use verbatim curly brackets in the template body, they must be
//! escaped as `@{` and `@}`, the same goes for the `@` sign, that
//! precedes expressions and special blocks; verbtim `@` signs must be
//! escaped as `@@`.
//!
//! [`ToHtml`]: crate::templates::ToHtml
//! [`Display`]: std::fmt::Display
#![allow(non_snake_case)]
pub mod a_Value_expressions {
//! A value expression can be as simple as `@name` to get the value of
//! a parameter, but more complicated expressions, including function
//! calls, are also allowed.
//!
//! # Value expressions
//!
//! A parameter can be used in an expression preceded by an @ sign.
//!
//! ```text
//! <h1>@name</h1>
//! ```
//!
//! If a parameter is a struct or a trait object, its fields or methods can
//! be used, and if it is a callable, it can be called.
//!
//! ```text
//! <p>The user @user.name has email @user.get_email().</p>
//! <p>A function result is @function(with, three, arguments).</p>
//! ```
//!
//! Standard function and macros can also be used, e.g. for specific
//! formatting needs:
//!
//! ```text
//! <p>The value is @format!("{:.1}", float_value).</p>
//! ```
//!
//! If more complex expressions are needed, they can be put in
//! parenthesis.
//!
//! ```text
//! <p>The sum @a+3 is @(a+3).</p>
//! ```
//! If `a` is 2, this exapands to:
//! ```text
//! <p>The sum 2+3 is 5.</p>
//! ```
//!
//! Anything is allowed in parenthesis, as long as parenthesis,
//! brackets and string quotes are balanced.
//! Note that this also applies to the parenthesis of a function
//! call or the brackets of an index, so complex things like the
//! following are allowed:
//!
//! ```text
//! <p>Index: @myvec[t.map(|s| s.length()).unwrap_or(0)].</p>
//! <p>Argument: @call(a + 3, |t| t.something()).</p>
//! ```
//!
//! An expression ends when parenthesis and brackets are matched
//! and it is followed by something not allowed in an expression.
//! This includes whitespace and e.g. the `<` and `@` characters.
//! If an expression starts with an open parenthesis, the
//! expression ends when that parentheis is closed.
//! That is usefull if an expression is to be emmediatley followed
//! by something that would be allowed in an expression.
//!
//! ```text
//! <p>@arg</p>
//! <p>@arg.</p>
//! <p>@arg.@arg</p>
//! <p>@arg.len()</p>
//! <p>@(arg).len()</p>
//! <p>@((2_i8 - 3).abs())</p>@* Note extra parens needed here *@
//! ```
//! With `arg = "name"`, the above renders as:
//! ```text
//! <p>name</p>
//! <p>name.</p>
//! <p>name.name</p>
//! <p>4</p>
//! <p>name.len()</p>
//! <p>1</p>
//! ```
}
pub mod b_Loops {
//! A ructe `@for` loop works just as a rust `for` loop,
//! iterating over anything that implements `std::iter::IntoIterator`,
//! such as a `Vec` or a slice.
//!
//! # Loops
//!
//! Rust-like loops are supported like this:
//!
//! ```text
//! <ul>@for item in items {
//! <li>@item</li>
//! }</ul>
//! ```
//!
//! Note that the thing to loop over (items, in the example) is a rust
//! expression, while the contents of the block is template code.
//!
//! If items is a slice of tuples (or really, anything that is
//! iterable yielding tuples), it is possible to deconstruct the
//! tuples into separate values directly:
//!
//! ```text
//! @for (n, item) in items.iter().enumerate() {
//! <p>@n: @item</p>
//! }
//! ```
//!
//! It is also possible to loop over a literal array (which may be
//! an array of tuples), as long as you do it by reference:
//!
//! ```text
//! @for &(name, age) in &[("Rasmus", 44), ("Mike", 36)] {
//! <p>@name is @age years old.</p>
//! }
//! ```
}
pub mod c_Conditionals {
//! Both `@if` statements with boolean expressions, `@if let` guard
//! statements, and `@match` statements are supported.
//!
//! # Conditionals
//!
//! Rust-like conditionals are supported in a style similar to the loops:
//!
//! ```text
//! @if items.is_empty() {
//! <p>There are no items.</p>
//! }
//! ```
//!
//! Pattern matching let expressions are also supported, as well as an
//! optional else part.
//!
//! ```text
//! @if let Some(foo) = foo {
//! <p>Foo is @foo.</p>
//! } else {
//! <p>There is no foo.</p>
//! }
//! ```
//!
//! The condition or let expression should allow anything that would be
//! allowed in the same place in plain rust.
//! As with loops, the things in the curly brackets are ructe template
//! code.
//!
//! ## match
//!
//! Pattern matching using `match` statements are also supported.
//!
//! ```text
//! @match answer {
//! Ok(value) => {
//! <p>The answer is @value.</p>
//! }
//! Err(_) => {
//! <p>I don't know the answer.</p>
//! }
//! }
//! ```
//!
//! The let expression and patterns should allow anything that would be
//! allowed in the same place in plain rust.
//! As above, the things in the curly brackets are ructe template code.
}
pub mod d_Calling_other_templates {
//! The ability to call other templates for from a template makes
//! both "tag libraries" and "base templates" possible with the
//! same syntax.
//!
//! # Calling other templates
//!
//! While rust methods can be called as a simple expression, there is a
//! special syntax for calling other templates:
//! `@:template_name(template_arguments)`.
//! Also, before calling a template, it has to be imported by a `use`
//! statement.
//! Templates are declared in a `templates` module.
//!
//! So, given something like this in `header.rs.html`:
//!
//! ```text
//! @(title: &str)
//!
//! <head>
//! <title>@title</title>
//! <link rel="stylesheet" href="/my/style.css" type="text/css">
//! </head>
//! ```
//!
//! It can be used like this:
//!
//! ```text
//! @use super::header_html;
//!
//! @()
//!
//! <html>
//! @:header_html("Example")
//! <body>
//! <h1>Example</h1>
//! <p>page content ...</p>
//! </body>
//! </html>
//! ```
//!
//! It is also possible to send template blocks as parameters to templates.
//! A structure similar to the above can be created by having something like
//! this in `base_page.rs.html`:
//!
//! ```text
//! @(title: &str, body: Content)
//!
//! <html>
//! <head>
//! <title>@title</title>
//! <link rel="stylesheet" href="/my/style.css" type="text/css">
//! </head>
//! <body>
//! <h1>@title</h1>
//! @:body()
//! </body>
//! </html>
//! ```
//!
//! And use it like this:
//!
//! ```text
//! @use super::base_page_html;
//!
//! @()
//!
//! @:base_page_html("Example", {
//! <p>page content ...</p>
//! })
//! ```
}

View File

@ -0,0 +1,275 @@
use crate::parseresult::PResult;
use nom::branch::alt;
use nom::bytes::complete::{escaped, is_a, is_not, tag};
use nom::character::complete::{alpha1, char, digit1, none_of, one_of};
use nom::combinator::{map, map_res, not, opt, recognize, value};
use nom::error::context; //, VerboseError};
use nom::multi::{fold_many0, many0, separated_list0};
use nom::sequence::{delimited, pair, preceded, terminated, tuple};
use std::str::{from_utf8, Utf8Error};
pub fn expression(input: &[u8]) -> PResult<&str> {
map_res(
recognize(context(
"Expected rust expression",
tuple((
map_res(alt((tag("&"), tag("*"), tag(""))), input_to_str),
alt((
rust_name,
map_res(digit1, input_to_str),
quoted_string,
expr_in_parens,
expr_in_brackets,
)),
fold_many0(
alt((
preceded(context("separator", tag(".")), expression),
preceded(tag("::"), expression),
expr_in_parens,
expr_in_braces,
expr_in_brackets,
preceded(tag("!"), expr_in_parens),
preceded(tag("!"), expr_in_brackets),
)),
|| (),
|_, _| (),
),
)),
)),
input_to_str,
)(input)
}
pub fn input_to_str(s: &[u8]) -> Result<&str, Utf8Error> {
from_utf8(s)
}
pub fn comma_expressions(input: &[u8]) -> PResult<String> {
map(
separated_list0(preceded(tag(","), many0(tag(" "))), expression),
|list: Vec<_>| list.join(", "),
)(input)
}
pub fn rust_name(input: &[u8]) -> PResult<&str> {
map_res(
recognize(pair(
alt((tag("_"), alpha1)),
opt(is_a("_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")),
)),
input_to_str,
)(input)
}
fn expr_in_parens(input: &[u8]) -> PResult<&str> {
map_res(
recognize(delimited(tag("("), expr_inside_parens, tag(")"))),
input_to_str,
)(input)
}
fn expr_in_brackets(input: &[u8]) -> PResult<&str> {
map_res(
recognize(delimited(
tag("["),
many0(alt((
value((), is_not("[]()\"/")),
value((), expr_in_brackets),
value((), expr_in_braces),
value((), expr_in_parens),
value((), quoted_string),
value((), rust_comment),
value((), terminated(tag("/"), none_of("*"))),
))),
tag("]"),
)),
input_to_str,
)(input)
}
pub fn expr_in_braces(input: &[u8]) -> PResult<&str> {
map_res(
recognize(delimited(
tag("{"),
many0(alt((
value((), is_not("{}[]()\"/")),
value((), expr_in_brackets),
value((), expr_in_braces),
value((), expr_in_parens),
value((), quoted_string),
value((), rust_comment),
value((), terminated(tag("/"), none_of("*"))),
))),
tag("}"),
)),
input_to_str,
)(input)
}
pub fn expr_inside_parens(input: &[u8]) -> PResult<&str> {
map_res(
recognize(many0(alt((
value((), is_not("{}[]()\"/")),
value((), expr_in_braces),
value((), expr_in_brackets),
value((), expr_in_parens),
value((), quoted_string),
value((), rust_comment),
value((), terminated(tag("/"), none_of("*"))),
)))),
input_to_str,
)(input)
}
pub fn quoted_string(input: &[u8]) -> PResult<&str> {
map_res(
recognize(delimited(
char('"'),
opt(escaped(is_not("\"\\"), '\\', one_of("'\"\\nrt0xu"))),
char('"'),
)),
input_to_str,
)(input)
}
pub fn rust_comment(input: &[u8]) -> PResult<&[u8]> {
delimited(
tag("/*"),
recognize(many0(alt((
is_not("*"),
terminated(tag("*"), not(tag("/"))),
)))),
tag("*/"),
)(input)
}
#[cfg(test)]
mod test {
use super::expression;
#[test]
fn expression_1() {
check_expr("foo");
}
#[test]
fn expression_2() {
check_expr("x15");
}
#[test]
fn expression_3() {
check_expr("a_b_c");
}
#[test]
fn expression_4() {
check_expr("foo.bar");
}
#[test]
fn expression_5() {
check_expr("foo.bar.baz");
}
#[test]
fn expression_6() {
check_expr("(!foo.is_empty())");
}
#[test]
fn expression_7() {
check_expr("foo(x, a.b.c(), d)");
}
#[test]
fn expression_8() {
check_expr("foo(&\"x\").bar");
}
#[test]
fn expression_9() {
check_expr("foo().bar(x).baz");
}
#[test]
fn expression_str() {
check_expr("\"foo\"");
}
#[test]
fn expression_str_paren() {
check_expr("(\")\")");
}
#[test]
fn expression_str_quoted() {
check_expr("\"line 1\\nline\\t2\"");
}
#[test]
fn expression_str_quoted_unicode() {
check_expr("\"Snowman: \\u{2603}\"");
}
#[test]
fn expression_enum_variant() {
check_expr("MyEnum::Variant.method()");
}
#[test]
fn expression_str_with_escaped_quotes() {
check_expr("\"Hello \\\"world\\\"\"");
}
#[test]
fn expression_slice() {
check_expr("&[foo, bar]");
}
#[test]
fn expression_slice_empty() {
check_expr("&[]");
}
#[test]
fn expression_number() {
check_expr("42");
}
#[test]
fn expression_with_comment() {
check_expr("(42 /* truly important number */)");
}
#[test]
fn expression_with_comment_a() {
check_expr("(42 /* \" */)");
}
#[test]
fn expression_with_comment_b() {
check_expr("(42 /* ) */)");
}
#[test]
fn expression_arithemtic_in_parens() {
check_expr("(2 + 3*4 - 5/2)");
}
fn check_expr(expr: &str) {
assert_eq!(expression(expr.as_bytes()), Ok((&b""[..], expr)));
}
#[test]
fn non_expression_a() {
assert_eq!(
expression_error_message(b".foo"),
": 1:.foo\n\
: ^ Expected rust expression\n"
);
}
#[test]
fn non_expression_b() {
assert_eq!(
expression_error_message(b" foo"),
": 1: foo\n\
: ^ Expected rust expression\n"
);
}
#[test]
fn non_expression_c() {
assert_eq!(
expression_error_message(b"(missing end"),
": 1:(missing end\n\
: ^ Expected rust expression\n"
);
}
fn expression_error_message(input: &[u8]) -> String {
use crate::parseresult::show_errors;
let mut buf = Vec::new();
if let Err(error) = expression(input) {
show_errors(&mut buf, input, &error, ":");
}
String::from_utf8(buf).unwrap()
}
}

454
ructe-0.17.0/src/lib.rs Normal file
View File

@ -0,0 +1,454 @@
//! Rust Compiled Templates is a HTML template system for Rust.
//!
//! Ructe works by converting your templates (and static files) to
//! rust source code, which is then compiled with your project.
//! This has the benefits that:
//!
//! 1. Many syntactical and logical errors in templates are caught
//! compile-time, rather than in a running server.
//! 2. No extra latency on the first request, since the templates are
//! fully compiled before starting the program.
//! 3. The template files does not have to be distributed / installed.
//! Templates (and static assets) are included in the compiled
//! program, which can be a single binary.
//!
//! The template syntax, which is inspired by [Twirl], the Scala-based
//! template engine in [Play framework], is documented in
//! the [Template_syntax] module.
//! A sample template may look like this:
//!
//! ```html
//! @use any::rust::Type;
//!
//! @(name: &str, items: &[Type])
//!
//! <html>
//! <head><title>@name</title></head>
//! <body>
//! @if items.is_empty() {
//! <p>There are no items.</p>
//! } else {
//! <p>There are @items.len() items.</p>
//! <ul>
//! @for item in items {
//! <li>@item</li>
//! }
//! </ul>
//! }
//! <body>
//! </html>
//! ```
//!
//! There are some [examples in the repository].
//! There is also a separate example of
//! [using ructe with warp and diesel].
//!
//! [Twirl]: https://github.com/playframework/twirl
//! [Play framework]: https://www.playframework.com/
//! [examples in the repository]: https://github.com/kaj/ructe/tree/master/examples
//! [using ructe with warp and diesel]: https://github.com/kaj/warp-diesel-ructe-sample
//!
//! To be able to use this template in your rust code, you need a
//! `build.rs` that transpiles the template to rust code.
//! A minimal such build script looks like the following.
//! See the [`Ructe`] struct documentation for details.
//!
//! ```rust,no_run
//! use ructe::{Result, Ructe};
//!
//! fn main() -> Result<()> {
//! Ructe::from_env()?.compile_templates("templates")
//! }
//! ```
//!
//! When calling a template, the arguments declared in the template will be
//! prepended by a `Write` argument to write the output to.
//! It can be a `Vec<u8>` as a buffer or for testing, or an actual output
//! destination.
//! The return value of a template is `std::io::Result<()>`, which should be
//! `Ok(())` unless writing to the destination fails.
//!
//! ```
//! #[test]
//! fn test_hello() {
//! let mut buf = Vec::new();
//! templates::hello_html(&mut buf, "World").unwrap();
//! assert_eq!(buf, b"<h1>Hello World!</h1>\n");
//! }
//! ```
//!
//! # Optional features
//!
//! Ructe has some options that can be enabled from `Cargo.toml`.
//!
//! * `sass` -- Compile sass and include the compiled css as static assets.
//! * `mime03` -- Static files know their mime types, compatible with
//! version 0.3.x of the [mime] crate.
//! * `mime02` -- Static files know their mime types, compatible with
//! version 0.2.x of the [mime] crate.
//! * `warp03` -- Provide an extension to `Response::Builder` of the [warp]
//! framework (versions 0.3.x) to simplify template rendering.
//! * `http-types` -- Static files know their mime types, compatible with
//! the [http-types] crate.
//! * `tide013`, `tide014`, `tide015`, `tide016` -- Support for the
//! [tide] framework version 0.13.x through 0.16.x. Implies the
//! `http-types` feature (but does not require a direct http-types
//! requirement, as that is reexported by tide).
//! (these versions of tide is compatible enough that the features
//! are actually just aliases for the first one, but a future tide
//! version may require a modified feature.)
//!
//! [mime]: https://crates.rs/crates/mime
//! [warp]: https://crates.rs/crates/warp
//! [tide]: https://crates.rs/crates/tide
//! [http-types]: https://crates.rs/crates/http-types
//!
//! The `mime02`, `mime03`, and `http-types` features are mutually
//! exclusive and requires a dependency on a matching version of
//! `mime` or `http-types`.
//! Any of them can be combined with the `sass` feature.
//!
//! ```toml
//! build = "src/build.rs"
//!
//! [build-dependencies]
//! ructe = { version = "0.6.0", features = ["sass", "mime03"] }
//!
//! [dependencies]
//! mime = "0.3.13"
//! ```
#![forbid(unsafe_code, missing_docs)]
#![allow(clippy::manual_strip)] // Until MSR is 1.45.0
pub mod Template_syntax;
mod expression;
mod parseresult;
mod spacelike;
mod staticfiles;
mod template;
mod templateexpression;
use parseresult::show_errors;
use std::env;
use std::error::Error;
use std::fmt::{self, Debug, Display};
use std::fs::{create_dir_all, read_dir, File};
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use template::template;
pub use staticfiles::StaticFiles;
/// The main build-time interface of ructe.
///
/// Your build script should create an instance of `Ructe` and use it
/// to compile templates and possibly get access to the static files
/// handler.
///
/// Ructe compiles your templates to rust code that should be compiled
/// with your other rust code, so it needs to be called before
/// compiling.
/// Assuming you use [cargo], it can be done like this:
///
/// First, specify a build script and ructe as a build dependency in
/// `Cargo.toml`:
///
/// ```toml
/// build = "src/build.rs"
///
/// [build-dependencies]
/// ructe = "0.6.0"
/// ```
///
/// Then, in `build.rs`, compile all templates found in the templates
/// directory and put the output where cargo tells it to:
///
/// ```rust,no_run
/// use ructe::{Result, Ructe};
///
/// fn main() -> Result<()> {
/// Ructe::from_env()?.compile_templates("templates")
/// }
/// ```
///
/// And finally, include and use the generated code in your code.
/// The file `templates.rs` will contain `mod templates { ... }`,
/// so I just include it in my `main.rs`:
///
/// ```rust,ignore
/// include!(concat!(env!("OUT_DIR"), "/templates.rs"));
/// ```
///
///
/// When creating a `Ructe` it will create a file called
/// `templates.rs` in your `$OUT_DIR` (which is normally created and
/// specified by `cargo`).
/// The methods will add content, and when the `Ructe` goes of of
/// scope, the file will be completed.
///
/// [cargo]: https://doc.rust-lang.org/cargo/
pub struct Ructe {
f: Vec<u8>,
outdir: PathBuf,
}
impl Ructe {
/// Create a Ructe instance suitable for a [cargo]-built project.
///
/// A file called `templates.rs` (and a directory called
/// `templates` containing sub-modules) will be created in the
/// directory that cargo specifies with the `OUT_DIR` environment
/// variable.
///
/// [cargo]: https://doc.rust-lang.org/cargo/
pub fn from_env() -> Result<Ructe> {
Ructe::new(PathBuf::from(get_env("OUT_DIR")?))
}
/// Create a ructe instance writing to a given directory.
///
/// The `out_dir` path is assumed to be a directory that exists
/// and is writable.
/// A file called `templates.rs` (and a directory called
/// `templates` containing sub-modules) will be created in
/// `out_dir`.
///
/// If you are using Ructe in a project that uses [cargo],
/// you should probably use [`Ructe::from_env`] instead.
///
/// [cargo]: https://doc.rust-lang.org/cargo/
pub fn new(outdir: PathBuf) -> Result<Ructe> {
let mut f = Vec::with_capacity(512);
create_dir_all(&outdir)?;
//f.write_all(b"pub mod templates {\n")?;
write_if_changed(
&outdir.join("_utils.rs"),
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/templates/utils.rs"
)),
)?;
f.write_all(
b"#[doc(hidden)]\nmod _utils;\n\
#[doc(inline)]\npub use self::_utils::*;\n\n",
)?;
if cfg!(feature = "warp03") {
write_if_changed(
&outdir.join("_utils_warp03.rs"),
include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/templates/utils_warp03.rs"
)),
)?;
f.write_all(
b"#[doc(hidden)]\nmod _utils_warp03;\n\
#[doc(inline)]\npub use self::_utils_warp03::*;\n\n",
)?;
}
Ok(Ructe { f, outdir })
}
/// Create a `templates` module in `outdir` containing rust code for
/// all templates found in `indir`.
///
/// If indir is a relative path, it should be relative to the main
/// directory of your crate, i.e. the directory containing your
/// `Cargo.toml` file.
///
/// Files with suffix `.rs.html`, `.rs.svg`, or `.rs.xml` are
/// considered templates.
/// A templete file called `template.rs.html`, `template.rs.svg`,
/// etc, will result in a callable function named `template_html`,
/// `template_svg`, etc.
/// The `template_html` function will get a `template` alias for
/// backwards compatibility, but that will be removed in a future
/// release.
pub fn compile_templates<P>(&mut self, indir: P) -> Result<()>
where
P: AsRef<Path>,
{
handle_entries(&mut self.f, indir.as_ref(), &self.outdir)
}
/// Create a [`StaticFiles`] handler for this Ructe instance.
///
/// This will create a `statics` module inside the generated
/// `templates` module.
///
/// # Examples
///
/// This code goes into the `build.rs`:
///
/// ```no_run
/// # use ructe::{Ructe, RucteError};
/// # fn main() -> Result<(), RucteError> {
/// let mut ructe = Ructe::from_env()?;
/// ructe.statics()?.add_files("static")?;
/// Ok(())
/// # }
/// ```
///
/// Assuming your project have a directory named `static` that
/// contains e.g. a file called `logo.svg` and you have included
/// the generated `templates.rs`, you can now use
/// `templates::statics::logo_png` as a [`StaticFile`] in your
/// project.
///
/// [`StaticFile`]: templates::StaticFile
pub fn statics(&mut self) -> Result<StaticFiles> {
self.f.write_all(b"pub mod statics;")?;
StaticFiles::for_template_dir(
&self.outdir,
&PathBuf::from(get_env("CARGO_MANIFEST_DIR")?),
)
}
}
impl Drop for Ructe {
fn drop(&mut self) {
let _ = self.f.write_all(b"\n");
let _ =
write_if_changed(&self.outdir.join("mod.rs"), &self.f);
}
}
fn write_if_changed(path: &Path, content: &[u8]) -> io::Result<()> {
use std::fs::{read, write};
if let Ok(old) = read(path) {
if old == content {
return Ok(());
}
}
write(path, content)
}
fn handle_entries(
f: &mut impl Write,
indir: &Path,
outdir: &Path,
) -> Result<()> {
println!("cargo:rerun-if-changed={}", indir.display());
for entry in read_dir(indir)? {
let entry = entry?;
let path = entry.path();
if entry.file_type()?.is_dir() {
if let Some(filename) = entry.file_name().to_str() {
let outdir = outdir.join(filename);
create_dir_all(&outdir)?;
let mut modrs = Vec::with_capacity(512);
modrs.write_all(
b"#[allow(renamed_and_removed_lints)]\n\
#[cfg_attr(feature=\"cargo-clippy\", \
allow(useless_attribute))]\n\
#[allow(unused)]\n\
use super::{Html,ToHtml};\n",
)?;
handle_entries(&mut modrs, &path, &outdir)?;
write_if_changed(&outdir.join("mod.rs"), &modrs)?;
writeln!(f, "pub mod {filename};\n")?;
}
} else if let Some(filename) = entry.file_name().to_str() {
if filename.contains(".rs.") {
println!("cargo:rerun-if-changed={}", path.display());
let (prename, suffix) = filename.rsplit_once(".rs.").unwrap();
let name = format!("{prename}_{suffix}");
if handle_template(&name, &path, outdir)? {
writeln!(
f,
"#[doc(hidden)]\n\
mod template_{name};\n\
#[doc(inline)]\n\
pub use self::template_{name}::{name};\n",
)?;
}
}
}
}
Ok(())
}
fn handle_template(
name: &str,
path: &Path,
outdir: &Path,
) -> io::Result<bool> {
let mut input = File::open(path)?;
let mut buf = Vec::new();
input.read_to_end(&mut buf)?;
match template(&buf) {
Ok((_, t)) => {
let mut data = Vec::new();
t.write_rust(&mut data, name)?;
write_if_changed(
&outdir.join(format!("template_{name}.rs")),
&data,
)?;
Ok(true)
}
Err(error) => {
println!("cargo:warning=Template parse error in {path:?}:");
show_errors(&mut io::stdout(), &buf, &error, "cargo:warning=");
Ok(false)
}
}
}
pub mod templates;
fn get_env(name: &str) -> Result<String> {
env::var(name).map_err(|e| RucteError::Env(name.into(), e))
}
/// The build-time error type for Ructe.
pub enum RucteError {
/// A build-time IO error in Ructe
Io(io::Error),
/// Error resolving a given environment variable.
Env(String, env::VarError),
/// Error bundling a sass stylesheet as css.
#[cfg(feature = "sass")]
Sass(rsass::Error),
}
impl Error for RucteError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match &self {
RucteError::Io(e) => Some(e),
RucteError::Env(_, e) => Some(e),
#[cfg(feature = "sass")]
RucteError::Sass(e) => Some(e),
}
}
}
impl Display for RucteError {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
write!(out, "Error: {self:?}")
}
}
impl Debug for RucteError {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
match self {
RucteError::Io(err) => Display::fmt(err, out),
RucteError::Env(var, err) => write!(out, "{var:?}: {err}"),
#[cfg(feature = "sass")]
RucteError::Sass(err) => Debug::fmt(err, out),
}
}
}
impl From<io::Error> for RucteError {
fn from(e: io::Error) -> RucteError {
RucteError::Io(e)
}
}
#[cfg(feature = "sass")]
impl From<rsass::Error> for RucteError {
fn from(e: rsass::Error) -> RucteError {
RucteError::Sass(e)
}
}
/// A result where the error type is a [`RucteError`].
pub type Result<T> = std::result::Result<T, RucteError>;

View File

@ -0,0 +1,70 @@
use nom::error::{VerboseError, VerboseErrorKind};
use nom::{Err, IResult};
use std::io::Write;
use std::str::from_utf8;
/// Parser result, with verbose error.
pub type PResult<'a, O> = IResult<&'a [u8], O, VerboseError<&'a [u8]>>;
pub fn show_errors(
out: &mut impl Write,
buf: &[u8],
error: &Err<VerboseError<&[u8]>>,
prefix: &str,
) {
match error {
Err::Failure(VerboseError { ref errors })
| Err::Error(VerboseError { ref errors }) => {
for (rest, err) in errors.iter().rev() {
if let Some(message) = get_message(err) {
let pos = buf.len() - rest.len();
show_error(out, buf, pos, &message, prefix);
}
}
}
Err::Incomplete(needed) => {
let msg = format!("Incomplete: {needed:?}");
show_error(out, buf, 0, &msg, prefix);
}
}
}
fn get_message(err: &VerboseErrorKind) -> Option<String> {
match err {
VerboseErrorKind::Context(msg) => Some((*msg).into()),
VerboseErrorKind::Char(ch) => Some(format!("Expected {ch:?}")),
VerboseErrorKind::Nom(_err) => None,
}
}
fn show_error(
out: &mut impl Write,
buf: &[u8],
pos: usize,
msg: &str,
prefix: &str,
) {
let mut line_start = buf[0..pos].rsplitn(2, |c| *c == b'\n');
let _ = line_start.next();
let line_start = line_start.next().map_or(0, |bytes| bytes.len() + 1);
let line = buf[line_start..]
.splitn(2, |c| *c == b'\n')
.next()
.and_then(|s| from_utf8(s).ok())
.unwrap_or("(Failed to display line)");
let line_no = bytecount::count(&buf[..line_start], b'\n') + 1;
let pos_in_line =
from_utf8(&buf[line_start..pos]).unwrap().chars().count() + 1;
writeln!(
out,
"{prefix}{:>4}:{}\n\
{prefix} {:>pos$} {}",
line_no,
line,
"^",
msg,
pos = pos_in_line,
prefix = prefix,
)
.unwrap();
}

View File

@ -0,0 +1,99 @@
use crate::parseresult::PResult;
use nom::branch::alt;
use nom::bytes::complete::{is_not, tag};
use nom::character::complete::{multispace1, none_of};
use nom::combinator::{map, value};
use nom::multi::many0;
use nom::sequence::preceded;
pub fn spacelike(input: &[u8]) -> PResult<()> {
map(many0(alt((comment, map(multispace1, |_| ())))), |_| ())(input)
}
pub fn comment(input: &[u8]) -> PResult<()> {
preceded(tag("@*"), comment_tail)(input)
}
pub fn comment_tail(input: &[u8]) -> PResult<()> {
preceded(
many0(alt((
value((), is_not("*")),
value((), preceded(tag("*"), none_of("@"))),
))),
value((), tag("*@")),
)(input)
}
#[cfg(test)]
mod test {
use super::{comment, spacelike};
use nom::error::{ErrorKind, VerboseError, VerboseErrorKind};
use nom::Err;
#[test]
fn comment1() {
assert_eq!(comment(b"@* a simple comment *@"), Ok((&b""[..], ())));
}
#[test]
fn comment2() {
let space_before = b" @* comment *@";
assert_eq!(
comment(space_before),
Err(Err::Error(VerboseError {
errors: vec![(
&space_before[..],
VerboseErrorKind::Nom(ErrorKind::Tag),
)],
})),
)
}
#[test]
fn comment3() {
assert_eq!(
comment(b"@* comment *@ & stuff"),
Ok((&b" & stuff"[..], ()))
);
}
#[test]
fn comment4() {
assert_eq!(
comment(b"@* comment *@ and @* another *@"),
Ok((&b" and @* another *@"[..], ()))
);
}
#[test]
fn comment5() {
assert_eq!(
comment(b"@* comment containing * and @ *@"),
Ok((&b""[..], ()))
);
}
#[test]
fn comment6() {
assert_eq!(
comment(b"@*** peculiar comment ***@***"),
Ok((&b"***"[..], ()))
);
}
#[test]
fn spacelike_empty() {
assert_eq!(spacelike(b""), Ok((&b""[..], ())));
}
#[test]
fn spacelike_simple() {
assert_eq!(spacelike(b" "), Ok((&b""[..], ())));
}
#[test]
fn spacelike_long() {
assert_eq!(
spacelike(
b"\n\
@* a comment on a line by itself *@\n\
\t\t \n\n\r\n\
@*another comment*@ something else"
),
Ok((&b"something else"[..], ()))
);
}
}

View File

@ -0,0 +1,708 @@
use super::Result;
use itertools::Itertools;
use std::ascii::escape_default;
use std::collections::BTreeMap;
use std::fmt::{self, Display};
use std::fs::{read_dir, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
/// Handler for static files.
///
/// Apart from handling templates for dynamic content, ructe also
/// helps with constants for static content.
///
/// Most sites that need HTML templates also needs some static resources.
/// Maybe one or several CSS files, some javascript, and / or pictures.
/// A good way to reduce network round-trips is to use a far expires
/// header to tell the browser it can cache those files and don't need
/// to check if they have changed.
/// But what if the files do change?
/// Then pretty much the only way to make sure the browser gets the
/// updated file is to change the URL to the file as well.
///
/// Ructe can create content-dependent file names for static files.
/// If you have an `image.png`, ructe may call it `image-SomeHash.png`
/// where `SomeHash` is 8 url-safe base64 characters encoding 48 bits
/// of a md5 sum of the file.
///
/// Each static file will be available as a
/// [`StaticFile`](templates/statics/index.html) struct instance in
/// your `templates::statics` module.
/// Also, the const `STATICS` array in the same module will contain a
/// reference to each of those instances.
///
/// Actually serving the file is a job for a web framework like
/// [iron](https://github.com/iron/iron),
/// [nickel](https://github.com/nickel-org/nickel.rs) or
/// [rocket](https://rocket.rs/), but ructe helps by packing the file
/// contents into a constant struct that you can access from rust
/// code.
///
/// # Overview
///
/// This section describes how to set up your project to serve
/// static content using ructe.
///
/// To do this, the first step is to add a line in `build.rs` telling
/// ructe to find and transpile your static files:
///
/// ```no_run
/// # use ructe::{Ructe, RucteError};
/// # fn main() -> Result<(), RucteError> {
/// let mut ructe = Ructe::from_env()?;
/// ructe.statics()?.add_files("static")?;
/// # Ok(())
/// # }
/// ```
///
/// Then you need to link to the encoded file.
/// For an image, you probably want to link it from an `<img>` tag in
/// a template. That can be done like this:
///
/// ```html
/// @use super::statics::image_png;
/// @()
/// <img alt="Something" src="/static/@image_png.name">
/// ```
///
/// So, what has happened here?
/// First, assuming the `static` directory in your
/// `$CARGO_MANIFEST_DIR` contained a file name `image.png`, your
/// `templates::statics` module (which is reachable as `super::statics`
/// from inside a template) will contain a
/// `pub static image_png: StaticFile` which can be imported and used
/// in both templates and rust code.
/// A `StaticFile` has a field named `name` which is a `&'static str`
/// containing the name with the generated hash, `image-SomeHash.png`.
///
/// The next step is that a browser actually sends a request for
/// `/static/image-SomeHash.png` and your server needs to deliver it.
/// Here, things depend on your web framework, so we start with some
/// pseudo code.
/// Full examples for [warp], [gotham], [nickel], and [iron] is
/// available [in the ructe repository].
///
/// [warp]: https://crates.rs/crates/warp
/// [gotham]: https://crates.rs/crates/gotham
/// [nickel]: https://crates.rs/crates/nickel
/// [iron]: https://crates.rs/crates/iron
/// [in the ructe repository]: https://github.com/kaj/ructe/tree/master/examples
///
/// ```ignore
/// /// A hypothetical web framework calls this each /static/... request,
/// /// with the name component of the URL as the name argument.
/// fn serve_static(name: &str) -> Response {
/// if let Some(data) = StaticFile::get(name) {
/// Response::Ok(data.content)
/// } else {
/// Response::NotFound
/// }
/// }
/// ```
///
/// The `StaticFile::get` function returns the `&'static StaticFile`
/// for a given file name if the file exists.
/// This is a reference to the same struct that we used by the name
/// `image_png` in the template.
/// Besides the `name` field (which will be equal to the argument, or
/// `get` would not have returned this `StaticFile`), there is a
/// `content: &'static [u8]` field which contains the actual file
/// data.
///
/// # Content-types
///
/// How to get the content type of static files.
///
/// Ructe has support for making the content-type of each static
/// file availiable using the
/// [mime](https://crates.io/crates/mime) crate.
/// Since mime version 0.3.0 was a breaking change of how the
/// `mime::Mime` type was implemented, and both Nickel and Iron
/// currently require the old version (0.2.x), ructe provides
/// support for both mime 0.2.x and mime 0.3.x with separate
/// feature flags.
///
/// # Mime 0.2.x
///
/// To use the mime 0.2.x support, enable the `mime02` feature and
/// add mime 0.2.x as a dependency:
///
/// ```toml
/// [build-dependencies]
/// ructe = { version = "^0.3.2", features = ["mime02"] }
///
/// [dependencies]
/// mime = "~0.2"
/// ```
///
/// A `Mime` as implemented in `mime` version 0.2.x cannot be
/// created statically, so instead a `StaticFile` provides
/// `pub fn mime(&self) -> Mime`.
///
/// ```
/// # // Test and doc even without the feature, so mock functionality.
/// # pub mod templates { pub mod statics {
/// # pub struct FakeFile;
/// # impl FakeFile { pub fn mime(&self) -> &'static str { "image/png" } }
/// # pub static image_png: FakeFile = FakeFile;
/// # }}
/// use templates::statics::image_png;
///
/// # fn main() {
/// assert_eq!(format!("Type is {}", image_png.mime()),
/// "Type is image/png");
/// # }
/// ```
///
/// # Mime 0.3.x
///
/// To use the mime 0.3.x support, enable the `mime3` feature and
/// add mime 0.3.x as a dependency:
///
/// ```toml
/// [build-dependencies]
/// ructe = { version = "^0.3.2", features = ["mime03"] }
///
/// [dependencies]
/// mime = "~0.3"
/// ```
///
/// From version 0.3, the `mime` crates supports creating const
/// static `Mime` objects, so with this feature, a `StaticFile`
/// simply has a `pub mime: &'static Mime` field.
///
/// ```
/// # // Test and doc even without the feature, so mock functionality.
/// # pub mod templates { pub mod statics {
/// # pub struct FakeFile { pub mime: &'static str }
/// # pub static image_png: FakeFile = FakeFile { mime: "image/png", };
/// # }}
/// use templates::statics::image_png;
///
/// # fn main() {
/// assert_eq!(format!("Type is {}", image_png.mime),
/// "Type is image/png");
/// # }
/// ```
pub struct StaticFiles {
/// Rust source file `statics.rs` beeing written.
src: Vec<u8>,
/// Path for writing the file `statics.rs`.
src_path: PathBuf,
/// Base path for finding static files with relative paths
base_path: PathBuf,
/// Maps rust names to public names (foo_jpg -> foo-abc123.jpg)
names: BTreeMap<String, String>,
/// Maps public names to rust names (foo-abc123.jpg -> foo_jpg)
names_r: BTreeMap<String, String>,
}
impl StaticFiles {
pub(crate) fn for_template_dir(
outdir: &Path,
base_path: &Path,
) -> Result<Self> {
let mut src = Vec::with_capacity(512);
if cfg!(feature = "mime03") {
src.write_all(b"use mime::Mime;\n\n")?;
}
if cfg!(feature = "tide013") {
src.write_all(b"use tide::http::mime::{self, Mime};\n\n")?;
} else if cfg!(feature = "http-types") {
src.write_all(b"use http_types::mime::{self, Mime};\n\n")?;
}
src.write_all(
b"/// A static file has a name (so its url can be recognized) and the
/// actual file contents.
///
/// The name includes a short (48 bits as 8 base64 characters) hash of
/// the content, to enable long-time caching of static resourses in
/// the clients.
#[allow(dead_code)]
pub struct StaticFile {
pub content: &'static [u8],
pub name: &'static str,
")?;
if cfg!(feature = "mime02") {
src.write_all(b" _mime: &'static str,\n")?;
}
if cfg!(feature = "mime03") {
src.write_all(b" pub mime: &'static Mime,\n")?;
}
if cfg!(feature = "http-types") {
src.write_all(b" pub mime: &'static Mime,\n")?;
}
src.write_all(
b"}
#[allow(dead_code)]
impl StaticFile {
/// Get a single `StaticFile` by name, if it exists.
#[must_use]
pub fn get(name: &str) -> Option<&'static Self> {
if let Ok(pos) = STATICS.binary_search_by_key(&name, |s| s.name) {
Some(STATICS[pos])
} else {None}
}
}
",
)?;
if cfg!(feature = "mime02") {
src.write_all(
b"use mime::Mime;
impl StaticFile {
/// Get the mime type of this static file.
///
/// Currently, this method parses a (static) string every time.
/// A future release of `mime` may support statically created
/// `Mime` structs, which will make this nicer.
#[allow(unused)]
pub fn mime(&self) -> Mime {
self._mime.parse().unwrap()
}
}
",
)?;
}
Ok(StaticFiles {
src,
src_path: outdir.join("statics.rs"),
base_path: base_path.into(),
names: BTreeMap::new(),
names_r: BTreeMap::new(),
})
}
// Should the return type be some kind of cow path?
fn path_for(&self, path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref();
if path.is_relative() {
self.base_path.join(path)
} else {
path.into()
}
}
/// Add all files from a specific directory, `indir`, as static files.
pub fn add_files(
&mut self,
indir: impl AsRef<Path>,
) -> Result<&mut Self> {
let indir = self.path_for(indir);
println!("cargo:rerun-if-changed={}", indir.display());
for entry in read_dir(indir)? {
let entry = entry?;
if entry.file_type()?.is_file() {
self.add_file(&entry.path())?;
}
}
Ok(self)
}
/// Add all files from a specific directory, `indir`, as static files.
///
/// The `to` string is used as a directory path of the resulting
/// urls, the file names are taken as is, without adding any hash.
/// This is usefull for resources used by preexisting javascript
/// packages, where it might be hard to change the used urls.
///
/// Note that some way of changing the url when the content
/// changes is still needed if you serve the files with far
/// expire, and using this method makes that your responsibility
/// rather than ructes.
/// Either the file may have hashed names as is, or you may use
/// the version number of a 3:rd party package as part of the `to`
/// parameter.
///
/// The `to` parameter may be an empty string.
/// In that case, no extra slash is added.
pub fn add_files_as(
&mut self,
indir: impl AsRef<Path>,
to: &str,
) -> Result<&mut Self> {
for entry in read_dir(self.path_for(indir))? {
let entry = entry?;
let file_type = entry.file_type()?;
let to = if to.is_empty() {
entry.file_name().to_string_lossy().to_string()
} else {
format!("{}/{}", to, entry.file_name().to_string_lossy())
};
if file_type.is_file() {
self.add_file_as(&entry.path(), &to)?;
} else if file_type.is_dir() {
self.add_files_as(&entry.path(), &to)?;
}
}
Ok(self)
}
/// Add one specific file as a static file.
///
/// Create a name to use in the url like `name-hash.ext` where
/// name and ext are the name and extension from `path` and has is
/// a few url-friendly bytes from a hash of the file content.
///
pub fn add_file(&mut self, path: impl AsRef<Path>) -> Result<&mut Self> {
let path = self.path_for(path);
if let Some((name, ext)) = name_and_ext(&path) {
println!("cargo:rerun-if-changed={}", path.display());
let mut input = File::open(&path)?;
let mut buf = Vec::new();
input.read_to_end(&mut buf)?;
let rust_name = format!("{name}_{ext}");
let url_name = format!("{name}-{}.{ext}", checksum_slug(&buf));
self.add_static(
&path,
&rust_name,
&url_name,
&FileContent(&path),
ext,
)?;
}
Ok(self)
}
/// Add one specific file as a static file.
///
/// Use `url_name` in the url without adding any hash characters.
pub fn add_file_as(
&mut self,
path: impl AsRef<Path>,
url_name: &str,
) -> Result<&mut Self> {
let path = &self.path_for(path);
let ext = name_and_ext(path).map_or("", |(_, e)| e);
println!("cargo:rerun-if-changed={}", path.display());
self.add_static(path, url_name, url_name, &FileContent(path), ext)?;
Ok(self)
}
/// Add a resource by its name and content, without reading an actual file.
///
/// The `path` parameter is used only to create a file name, the actual
/// content of the static file will be the `data` parameter.
/// A hash will be added to the file name, just as for
/// file-sourced statics.
///
/// # Examples
///
/// With the folloing code in `build.rs`:
/// ````
/// # use ructe::{Result, Ructe, StaticFiles};
/// # use std::fs::create_dir_all;
/// # use std::path::PathBuf;
/// # use std::vec::Vec;
/// # fn main() -> Result<()> {
/// # let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target").join("test-tmp").join("add-file");
/// # create_dir_all(&p);
/// # let mut ructe = Ructe::new(p)?;
/// let mut statics = ructe.statics()?;
/// statics.add_file_data("black.css", b"body{color:black}\n");
/// # Ok(())
/// # }
/// ````
///
/// A `StaticFile` named `black_css` will be defined in the
/// `templates::statics` module of your crate:
///
/// ````
/// # mod statics {
/// # use ructe::templates::StaticFile;
/// # pub static black_css: StaticFile = StaticFile {
/// # content: b"body{color:black}\n",
/// # name: "black-r3rltVhW.css",
/// # #[cfg(feature = "mime03")]
/// # mime: &mime::TEXT_CSS,
/// # };
/// # }
/// assert_eq!(statics::black_css.name, "black-r3rltVhW.css");
/// ````
pub fn add_file_data<P>(
&mut self,
path: P,
data: &[u8],
) -> Result<&mut Self>
where
P: AsRef<Path>,
{
let path = &self.path_for(path);
if let Some((name, ext)) = name_and_ext(path) {
let rust_name = format!("{name}_{ext}");
let url_name = format!("{name}-{}.{ext}", checksum_slug(data));
self.add_static(
path,
&rust_name,
&url_name,
&ByteString(data),
ext,
)?;
}
Ok(self)
}
/// Compile a sass file and add the resulting css.
///
/// If `src` is `"somefile.sass"`, then that file will be copiled
/// with [rsass] (using the `Comressed` output style).
/// The result will be added as if if was an existing
/// `"somefile.css"` file.
///
/// While handling the scss input, rsass is extended with a
/// `static_name` sass function that takes a file name as given to
/// [`add_file()`][Self::add_file] (or simliar) and returns the
/// `name-hash.ext` filename that ructe creates for it.
/// Note that only files that are added to the `StaticFiles`
/// _before_ the call to `add_sass_files` are supported by the
/// `static_name` function.
///
/// This method is only available when ructe is built with the
/// "sass" feature.
#[cfg(feature = "sass")]
pub fn add_sass_file<P>(&mut self, src: P) -> Result<&mut Self>
where
P: AsRef<Path>,
{
use rsass::css::CssString;
use rsass::input::CargoContext;
use rsass::output::{Format, Style};
use rsass::sass::{CallError, FormalArgs};
use rsass::value::Quotes;
use rsass::*;
use std::sync::Arc;
let format = Format {
style: Style::Compressed,
precision: 4,
};
let src = self.path_for(src);
let (context, scss) =
CargoContext::for_path(&src).map_err(rsass::Error::from)?;
let mut context = context.with_format(format);
let existing_statics = self.get_names().clone();
context.get_scope().define_function(
"static_name".into(),
sass::Function::builtin(
"",
&"static_name".into(),
FormalArgs::new(vec![("name".into(), None)]),
Arc::new(move |s| {
let name: String = s.get("name".into())?;
let rname = name.replace('-', "_").replace('.', "_");
existing_statics
.iter()
.find(|(n, _v)| *n == &rname)
.map(|(_n, v)| {
CssString::new(v.into(), Quotes::Double).into()
})
.ok_or_else(|| {
CallError::msg(format!(
"Static file {name:?} not found",
))
})
}),
),
);
let css = context.transform(scss)?;
self.add_file_data(&src.with_extension("css"), &css)
}
fn add_static(
&mut self,
path: &Path,
rust_name: &str,
url_name: &str,
content: &impl Display,
suffix: &str,
) -> Result<&mut Self> {
let mut rust_name =
rust_name.replace(|c: char| !c.is_alphanumeric(), "_");
if rust_name
.as_bytes()
.first()
.map(|c| c.is_ascii_digit())
.unwrap_or(true)
{
rust_name.insert(0, 'n');
}
writeln!(
self.src,
"\n/// From {path:?}\
\n#[allow(non_upper_case_globals)]\
\npub static {rust_name}: StaticFile = StaticFile {{\
\n content: {content},\
\n name: \"{url_name}\",\
\n{mime}\
}};",
path = path,
rust_name = rust_name,
url_name = url_name,
content = content,
mime = mime_arg(suffix),
)?;
self.names.insert(rust_name.clone(), url_name.into());
self.names_r.insert(url_name.into(), rust_name);
Ok(self)
}
/// Get a mapping of names, from without hash to with.
///
/// ````
/// # use ructe::{Result, Ructe, StaticFiles};
/// # use std::fs::create_dir_all;
/// # use std::path::PathBuf;
/// # use std::vec::Vec;
/// # fn main() -> Result<()> {
/// # let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target").join("test-tmp").join("get-names");
/// # create_dir_all(&p);
/// # let mut ructe = Ructe::new(p)?;
/// let mut statics = ructe.statics()?;
/// statics.add_file_data("black.css", b"body{color:black}\n");
/// statics.add_file_data("blue.css", b"body{color:blue}\n");
/// assert_eq!(
/// statics.get_names().iter()
/// .map(|(a, b)| format!("{} -> {}", a, b))
/// .collect::<Vec<_>>(),
/// vec!["black_css -> black-r3rltVhW.css".to_string(),
/// "blue_css -> blue-GZGxfXag.css".to_string()],
/// );
/// # Ok(())
/// # }
/// ````
pub fn get_names(&self) -> &BTreeMap<String, String> {
&self.names
}
}
impl Drop for StaticFiles {
/// Write the ending of the statics source code, declaring the
/// `STATICS` variable.
fn drop(&mut self) {
// Ignore a possible write failure, rather than a panic in drop.
let _ = writeln!(
self.src,
"\npub static STATICS: &[&StaticFile] \
= &[{}];",
self.names_r
.iter()
.map(|s| format!("&{}", s.1))
.format(", "),
);
let _ = super::write_if_changed(&self.src_path, &self.src);
}
}
struct FileContent<'a>(&'a Path);
impl<'a> Display for FileContent<'a> {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
write!(out, "include_bytes!({:?})", self.0)
}
}
struct ByteString<'a>(&'a [u8]);
impl<'a> Display for ByteString<'a> {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
out.write_str("b\"")?;
for byte in self.0 {
escape_default(*byte).fmt(out)?;
}
out.write_str("\"")
}
}
fn name_and_ext(path: &Path) -> Option<(&str, &str)> {
if let (Some(name), Some(ext)) = (path.file_name(), path.extension()) {
if let (Some(name), Some(ext)) = (name.to_str(), ext.to_str()) {
return Some((&name[..name.len() - ext.len() - 1], ext));
}
}
None
}
#[cfg(any(
all(feature = "mime03", feature = "http-types"),
all(feature = "mime02", feature = "http-types"),
all(feature = "mime02", feature = "mime03"),
))]
compile_error!(
r#"Only one of these features "http-types", "mime02" or "mime03" must be enabled at a time."#
);
/// A short and url-safe checksum string from string data.
fn checksum_slug(data: &[u8]) -> String {
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
BASE64_URL_SAFE_NO_PAD.encode(&md5::compute(data)[..6])
}
#[cfg(not(feature = "mime02"))]
#[cfg(not(feature = "mime03"))]
#[cfg(not(feature = "http-types"))]
fn mime_arg(_: &str) -> String {
"".to_string()
}
#[cfg(feature = "mime02")]
fn mime_arg(suffix: &str) -> String {
format!(" _mime: {:?},\n", mime_from_suffix(suffix))
}
#[cfg(feature = "mime02")]
fn mime_from_suffix(suffix: &str) -> &'static str {
match suffix.to_lowercase().as_ref() {
"bmp" => "image/bmp",
"css" => "text/css",
"eot" => "application/vnd.ms-fontobject",
"gif" => "image/gif",
"jpg" | "jpeg" => "image/jpeg",
"js" | "jsonp" => "application/javascript",
"json" => "application/json",
"png" => "image/png",
"svg" => "image/svg+xml",
"woff" => "font/woff",
"woff2" => "font/woff2",
_ => "application/octet-stream",
}
}
#[cfg(any(feature = "mime03", feature = "http-types"))]
fn mime_arg(suffix: &str) -> String {
format!(" mime: &mime::{},\n", mime_from_suffix(suffix))
}
#[cfg(feature = "mime03")]
fn mime_from_suffix(suffix: &str) -> &'static str {
// This is limited to the constants that is defined in mime 0.3.
match suffix.to_lowercase().as_ref() {
"bmp" => "IMAGE_BMP",
"css" => "TEXT_CSS",
"gif" => "IMAGE_GIF",
"jpg" | "jpeg" => "IMAGE_JPEG",
"js" | "jsonp" => "TEXT_JAVASCRIPT",
"json" => "APPLICATION_JSON",
"png" => "IMAGE_PNG",
"svg" => "IMAGE_SVG",
"woff" => "FONT_WOFF",
"woff2" => "FONT_WOFF",
_ => "APPLICATION_OCTET_STREAM",
}
}
#[cfg(feature = "http-types")]
fn mime_from_suffix(suffix: &str) -> &'static str {
match suffix.to_lowercase().as_ref() {
"css" => "CSS",
"html" | "htm" => "CSS",
"ico" => "ICO",
"jpg" | "jpeg" => "JPEG",
"js" | "jsonp" => "JAVASCRIPT",
"json" => "JSON",
"png" => "PNG",
"svg" => "SVG",
"txt" => "PLAIN",
"wasm" => "WASM",
"xml" => "XML",
_ => "mime::BYTE_STREAM",
}
}

View File

@ -0,0 +1,253 @@
use crate::expression::{input_to_str, rust_name};
use crate::parseresult::PResult;
use crate::spacelike::spacelike;
use crate::templateexpression::{template_expression, TemplateExpression};
use itertools::Itertools;
use nom::branch::alt;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::character::complete::{char, multispace0};
use nom::combinator::{map, map_res, opt, recognize};
use nom::error::context;
use nom::multi::{many0, many_till, separated_list0, separated_list1};
use nom::sequence::{delimited, preceded, terminated, tuple};
use std::io::{self, Write};
#[derive(Debug, PartialEq, Eq)]
pub struct Template {
preamble: Vec<String>,
type_args: String,
args: Vec<String>,
body: Vec<TemplateExpression>,
}
impl Template {
pub fn write_rust(
&self,
out: &mut impl Write,
name: &str,
) -> io::Result<()> {
out.write_all(
b"use std::io::{self, Write};\n\
#[allow(renamed_and_removed_lints)]\n\
#[cfg_attr(feature=\"cargo-clippy\", \
allow(useless_attribute))]\n\
#[allow(unused)]\n\
use super::{Html,ToHtml};\n",
)?;
for line in &self.preamble {
writeln!(out, "{line};")?;
}
writeln!(
out,
"\n\
#[allow(clippy::used_underscore_binding)]\n
pub fn {name}<{ta}{ta_sep}W>(#[allow(unused_mut)] mut _ructe_out_: W{args}) -> io::Result<()>\n\
where W: Write {{\n\
{body}\
Ok(())\n\
}}",
name = name,
ta = self.type_args,
ta_sep = if self.type_args.is_empty() { "" } else { ", " },
args =
self.args.iter().format_with("", |arg, f| f(&format_args!(
", {}",
arg.replace(
" Content",
" impl FnOnce(&mut W) -> io::Result<()>"
)
))),
body = self.body.iter().map(|b| b.code()).format(""),
)
}
}
pub fn template(input: &[u8]) -> PResult<Template> {
map(
tuple((
spacelike,
many0(map(
delimited(
tag("@"),
map_res(is_not(";()"), input_to_str),
terminated(tag(";"), spacelike),
),
String::from,
)),
context("expected '@('...')' template declaration.", tag("@")),
opt(delimited(
terminated(tag("<"), multispace0),
context(
"expected type argument or '>'",
map_res(
recognize(separated_list1(
terminated(tag(","), multispace0),
context(
"expected lifetime declaration",
preceded(tag("'"), rust_name),
),
)),
input_to_str,
),
),
tag(">"),
)),
delimited(
context(
"expected '('...')' template arguments declaration.",
terminated(tag("("), multispace0),
),
separated_list0(
terminated(tag(","), multispace0),
context(
"expected formal argument",
map(formal_argument, String::from),
),
),
context(
"expected ',' or ')'.",
delimited(multispace0, tag(")"), spacelike),
),
),
many_till(
context(
"Error in expression starting here:",
template_expression,
),
end_of_file,
),
)),
|((), preamble, _, type_args, args, body)| Template {
preamble,
type_args: type_args.map(String::from).unwrap_or_default(),
args,
body: body.0,
},
)(input)
}
fn end_of_file(input: &[u8]) -> PResult<()> {
if input.is_empty() {
Ok((input, ()))
} else {
use nom::error::{VerboseError, VerboseErrorKind};
Err(nom::Err::Error(VerboseError {
errors: vec![(input, VerboseErrorKind::Context("end of file"))],
}))
}
}
fn formal_argument(input: &[u8]) -> PResult<&str> {
map_res(
recognize(tuple((
rust_name,
spacelike,
char(':'),
spacelike,
type_expression,
))),
input_to_str,
)(input)
}
fn type_expression(input: &[u8]) -> PResult<()> {
map(
tuple((
alt((tag("&"), tag(""))),
opt(lifetime),
delimited(
spacelike,
alt((tag("impl"), tag("dyn"), tag(""))),
spacelike,
),
context(
"Expected rust type expression",
alt((
map(rust_name, |_| ()),
map(
delimited(tag("["), type_expression, tag("]")),
|_| (),
),
map(
delimited(tag("("), comma_type_expressions, tag(")")),
|_| (),
),
)),
),
opt(delimited(tag("<"), comma_type_expressions, tag(">"))),
)),
|_| (),
)(input)
}
pub fn comma_type_expressions(input: &[u8]) -> PResult<()> {
map(
terminated(
separated_list0(
preceded(tag(","), multispace0),
alt((type_expression, lifetime)),
),
opt(preceded(tag(","), multispace0)),
),
|_| (),
)(input)
}
fn lifetime(input: &[u8]) -> PResult<()> {
map(delimited(spacelike, tag("'"), rust_name), |_| ())(input)
}
#[cfg(test)]
mod test {
use super::type_expression;
#[test]
fn tuple() {
check_type_expr("(Foo, Bar)");
}
#[test]
fn unspaced_tuple() {
check_type_expr("(Foo,Bar)");
}
#[test]
fn tuple_with_trailing() {
check_type_expr("(Foo,Bar,)");
}
#[test]
fn generic() {
check_type_expr("HashMap<Foo, Bar>");
}
#[test]
fn unspaced_generic() {
check_type_expr("HashMap<Foo,Bar>");
}
#[test]
fn generic_with_trailing() {
check_type_expr("Vec<Foo,>");
}
#[test]
fn generic_with_lifetime() {
check_type_expr("SomeTypeWithRef<'a>");
}
#[test]
fn generic_with_anonymous_lifetime() {
check_type_expr("SomeTypeWithRef<'_>");
}
#[test]
fn multiword_constant() {
check_type_expr("ONE_TWO_THREE");
}
fn check_type_expr(expr: &str) {
assert_eq!(type_expression(expr.as_bytes()), Ok((&b""[..], ())));
}
}

View File

@ -0,0 +1,662 @@
use crate::expression::{
comma_expressions, expr_in_braces, expr_inside_parens, expression,
input_to_str, rust_name,
};
use crate::parseresult::PResult;
use crate::spacelike::{comment_tail, spacelike};
use itertools::Itertools;
use nom::branch::alt;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::character::complete::char;
use nom::combinator::{map, map_res, opt, recognize, value};
use nom::error::context;
use nom::multi::{many0, many_till, separated_list0};
use nom::sequence::{delimited, pair, preceded, terminated, tuple};
use std::fmt::{self, Display};
#[derive(Debug, PartialEq, Eq)]
pub enum TemplateExpression {
Comment,
Text {
text: String,
},
Expression {
expr: String,
},
ForLoop {
name: String,
expr: String,
body: Vec<TemplateExpression>,
},
IfBlock {
expr: String,
body: Vec<TemplateExpression>,
else_body: Option<Vec<TemplateExpression>>,
},
MatchBlock {
expr: String,
arms: Vec<(String, Vec<TemplateExpression>)>,
},
CallTemplate {
name: String,
args: Vec<TemplateArgument>,
},
}
#[derive(Debug, PartialEq, Eq)]
pub enum TemplateArgument {
Rust(String),
Body(Vec<TemplateExpression>),
}
impl Display for TemplateArgument {
fn fmt(&self, out: &mut fmt::Formatter) -> Result<(), fmt::Error> {
match *self {
TemplateArgument::Rust(ref s) => out.write_str(s),
TemplateArgument::Body(ref v) if v.is_empty() => {
out.write_str("|_| Ok(())")
}
TemplateArgument::Body(ref v) => writeln!(
out,
"#[allow(clippy::used_underscore_binding)] |mut _ructe_out_| {{\n{}\nOk(())\n}}",
v.iter().map(|b| b.code()).format(""),
),
}
}
}
impl TemplateExpression {
pub fn text(text: &str) -> Self {
TemplateExpression::Text {
text: text.to_string(),
}
}
pub fn code(&self) -> String {
match *self {
TemplateExpression::Comment => String::new(),
TemplateExpression::Text { ref text } if text.is_ascii() => {
format!("_ructe_out_.write_all(b{text:?})?;\n")
}
TemplateExpression::Text { ref text } => {
format!("_ructe_out_.write_all({text:?}.as_bytes())?;\n")
}
TemplateExpression::Expression { ref expr } => {
format!("{expr}.to_html(_ructe_out_.by_ref())?;\n")
}
TemplateExpression::ForLoop {
ref name,
ref expr,
ref body,
} => format!(
"for {name} in {expr} {{\n{}}}\n",
body.iter().map(|b| b.code()).format(""),
),
TemplateExpression::IfBlock {
ref expr,
ref body,
ref else_body,
} => format!(
"if {expr} {{\n{}}}{}\n",
body.iter().map(|b| b.code()).format(""),
match else_body.as_deref() {
Some([e @ TemplateExpression::IfBlock { .. }]) =>
format!(" else {}", e.code()),
Some(body) => format!(
" else {{\n{}}}",
body.iter().map(|b| b.code()).format(""),
),
None => String::new(),
}
),
TemplateExpression::MatchBlock { ref expr, ref arms } => format!(
"match {expr} {{{}}}\n",
arms.iter().format_with("", |(expr, body), f| {
f(&format_args!(
"\n {} => {{\n{}}}",
expr,
body.iter().map(|b| b.code()).format(""),
))
})
),
TemplateExpression::CallTemplate { ref name, ref args } => {
format!(
"{name}(_ructe_out_.by_ref(){})?;\n",
args.iter().format_with("", |arg, f| f(&format_args!(
", {arg}"
))),
)
}
}
}
}
pub fn template_expression(input: &[u8]) -> PResult<TemplateExpression> {
match opt(preceded(
char('@'),
alt((
tag("*"),
tag(":"),
tag("@"),
tag("{"),
tag("}"),
tag("("),
terminated(alt((tag("if"), tag("for"), tag("match"))), tag(" ")),
value(&b""[..], tag("")),
)),
))(input)?
{
(i, Some(b":")) => map(
pair(
rust_name,
delimited(
char('('),
separated_list0(
terminated(tag(","), spacelike),
template_argument,
),
char(')'),
),
),
|(name, args)| TemplateExpression::CallTemplate {
name: name.to_string(),
args,
},
)(i),
(i, Some(b"@")) => Ok((i, TemplateExpression::text("@"))),
(i, Some(b"{")) => Ok((i, TemplateExpression::text("{"))),
(i, Some(b"}")) => Ok((i, TemplateExpression::text("}"))),
(i, Some(b"*")) => {
map(comment_tail, |()| TemplateExpression::Comment)(i)
}
(i, Some(b"if")) => if2(i),
(i, Some(b"for")) => map(
tuple((
for_variable,
delimited(
terminated(
context("Expected \"in\"", tag("in")),
spacelike,
),
context("Expected iterable expression", loop_expression),
spacelike,
),
context("Error in loop block:", template_block),
)),
|(name, expr, body)| TemplateExpression::ForLoop {
name,
expr,
body,
},
)(i),
(i, Some(b"match")) => context(
"Error in match expression:",
map(
tuple((
delimited(spacelike, expression, spacelike),
preceded(
char('{'),
map(
many_till(
context(
"Error in match arm starting here:",
pair(
delimited(
spacelike,
map(expression, String::from),
spacelike,
),
preceded(
terminated(tag("=>"), spacelike),
template_block,
),
),
),
preceded(spacelike, char('}')),
),
|(arms, _end)| arms,
),
),
)),
|(expr, arms)| TemplateExpression::MatchBlock {
expr: expr.to_string(),
arms,
},
),
)(i),
(i, Some(b"(")) => {
map(terminated(expr_inside_parens, tag(")")), |expr| {
TemplateExpression::Expression {
expr: format!("({expr})"),
}
})(i)
}
(i, Some(b"")) => {
map(expression, |expr| TemplateExpression::Expression {
expr: expr.to_string(),
})(i)
}
(_i, Some(_)) => unreachable!(),
(i, None) => map(map_res(is_not("@{}"), input_to_str), |text| {
TemplateExpression::Text {
text: text.to_string(),
}
})(i),
}
}
fn if2(input: &[u8]) -> PResult<TemplateExpression> {
context(
"Error in conditional expression:",
map(
tuple((
delimited(spacelike, cond_expression, spacelike),
template_block,
opt(preceded(
delimited(spacelike, tag("else"), spacelike),
alt((
preceded(tag("if"), map(if2, |e| vec![e])),
template_block,
)),
)),
)),
|(expr, body, else_body)| TemplateExpression::IfBlock {
expr,
body,
else_body,
},
),
)(input)
}
fn for_variable(input: &[u8]) -> PResult<String> {
delimited(
spacelike,
context(
"Expected loop variable name or destructuring tuple",
alt((
map(
map_res(
recognize(preceded(rust_name, opt(expr_in_braces))),
input_to_str,
),
String::from,
),
map(
pair(
opt(char('&')),
delimited(char('('), comma_expressions, char(')')),
),
|(pre, args)| {
format!("{}({})", pre.map_or("", |_| "&"), args)
},
),
)),
),
spacelike,
)(input)
}
fn template_block(input: &[u8]) -> PResult<Vec<TemplateExpression>> {
preceded(
char('{'),
map(
many_till(
context(
"Error in expression starting here:",
template_expression,
),
char('}'),
),
|(block, _end)| block,
),
)(input)
}
fn template_argument(input: &[u8]) -> PResult<TemplateArgument> {
alt((
map(
delimited(
char('{'),
many0(template_expression),
terminated(char('}'), spacelike),
),
TemplateArgument::Body,
),
map(map(expression, String::from), TemplateArgument::Rust),
))(input)
}
fn cond_expression(input: &[u8]) -> PResult<String> {
match opt(tag("let"))(input)? {
(i, Some(b"let")) => map(
pair(
preceded(
spacelike,
context(
"Expected LHS expression in let binding",
expression,
),
),
preceded(
delimited(spacelike, char('='), spacelike),
context(
"Expected RHS expression in let binding",
expression,
),
),
),
|(lhs, rhs)| format!("let {lhs} = {rhs}"),
)(i),
(_i, Some(_)) => unreachable!(),
(i, None) => map(
context("Expected expression", logic_expression),
String::from,
)(i),
}
}
fn loop_expression(input: &[u8]) -> PResult<String> {
map(
map_res(
recognize(terminated(
expression,
opt(preceded(
terminated(tag(".."), opt(char('='))),
expression,
)),
)),
input_to_str,
),
String::from,
)(input)
}
fn logic_expression(input: &[u8]) -> PResult<&str> {
map_res(
recognize(tuple((
opt(terminated(char('!'), spacelike)),
expression,
opt(pair(
rel_operator,
context("Expected expression", logic_expression),
)),
))),
input_to_str,
)(input)
}
fn rel_operator(input: &[u8]) -> PResult<&str> {
map_res(
delimited(
spacelike,
context(
"Expected relational operator",
alt((
tag("!="),
tag("&&"),
tag("<="),
tag("<"),
tag("=="),
tag(">="),
tag(">"),
tag("||"),
)),
),
spacelike,
),
input_to_str,
)(input)
}
#[cfg(test)]
mod test {
use super::super::parseresult::show_errors;
use super::*;
#[test]
fn for_variable_simple() {
assert_eq!(
for_variable(b"foo").unwrap(),
(&b""[..], "foo".to_string())
)
}
#[test]
fn for_variable_tuple() {
assert_eq!(
for_variable(b"(foo, bar)").unwrap(),
(&b""[..], "(foo, bar)".to_string())
)
}
#[test]
fn for_variable_tuple_ref() {
assert_eq!(
for_variable(b"&(foo, bar)").unwrap(),
(&b""[..], "&(foo, bar)".to_string())
)
}
#[test]
fn for_variable_struct() {
assert_eq!(
for_variable(b"MyStruct{foo, bar}").unwrap(),
(&b""[..], "MyStruct{foo, bar}".to_string())
)
}
#[test]
fn call_simple() {
assert_eq!(
template_expression(b"@foo()"),
Ok((
&b""[..],
TemplateExpression::Expression {
expr: "foo()".to_string(),
},
))
)
}
/// Check that issue #53 stays fixed.
#[test]
fn call_empty_str() {
assert_eq!(
template_expression(b"@foo(\"\")"),
Ok((
&b""[..],
TemplateExpression::Expression {
expr: "foo(\"\")".to_string(),
},
))
)
}
#[test]
fn if_boolean_var() {
assert_eq!(
template_expression(b"@if cond { something }"),
Ok((
&b""[..],
TemplateExpression::IfBlock {
expr: "cond".to_string(),
body: vec![TemplateExpression::text(" something ")],
else_body: None,
}
))
)
}
#[test]
fn if_let() {
assert_eq!(
template_expression(b"@if let Some(x) = x { something }"),
Ok((
&b""[..],
TemplateExpression::IfBlock {
expr: "let Some(x) = x".to_string(),
body: vec![TemplateExpression::text(" something ")],
else_body: None,
}
))
)
}
#[test]
fn if_let_2() {
assert_eq!(
template_expression(b"@if let Some((x, y)) = x { something }"),
Ok((
&b""[..],
TemplateExpression::IfBlock {
expr: "let Some((x, y)) = x".to_string(),
body: vec![TemplateExpression::text(" something ")],
else_body: None,
}
))
)
}
#[test]
fn if_let_3() {
assert_eq!(
template_expression(
b"@if let Some(p) = Uri::borrow_from(&state) { something }"
),
Ok((
&b""[..],
TemplateExpression::IfBlock {
expr: "let Some(p) = Uri::borrow_from(&state)"
.to_string(),
body: vec![TemplateExpression::text(" something ")],
else_body: None,
}
))
)
}
#[test]
fn if_let_struct() {
assert_eq!(
template_expression(
b"@if let Struct{x, y} = variable { something }"
),
Ok((
&b""[..],
TemplateExpression::IfBlock {
expr: "let Struct{x, y} = variable".to_string(),
body: vec![TemplateExpression::text(" something ")],
else_body: None,
}
))
)
}
#[test]
fn if_compare() {
assert_eq!(
template_expression(b"@if x == 17 { something }"),
Ok((
&b""[..],
TemplateExpression::IfBlock {
expr: "x == 17".to_string(),
body: vec![TemplateExpression::text(" something ")],
else_body: None,
}
))
)
}
/// Check that issue #53 stays fixed.
#[test]
fn if_compare_empty_string() {
// Note that x.is_empty() would be better in real code, but this and
// other uses of empty strings in conditionals should be ok.
assert_eq!(
template_expression(b"@if x == \"\" { something }"),
Ok((
&b""[..],
TemplateExpression::IfBlock {
expr: "x == \"\"".to_string(),
body: vec![TemplateExpression::text(" something ")],
else_body: None,
}
))
)
}
#[test]
fn if_complex_logig() {
assert_eq!(
template_expression(b"@if x == 17 || y && z() { something }"),
Ok((
&b""[..],
TemplateExpression::IfBlock {
expr: "x == 17 || y && z()".to_string(),
body: vec![TemplateExpression::text(" something ")],
else_body: None,
}
))
)
}
#[test]
fn if_missing_conditional() {
assert_eq!(
expression_error(b"@if { oops }"),
": 1:@if { oops }\n\
: ^ Error in conditional expression:\n\
: 1:@if { oops }\n\
: ^ Expected expression\n\
: 1:@if { oops }\n\
: ^ Expected rust expression\n"
)
}
#[test]
fn if_bad_let() {
assert_eq!(
expression_error(b"@if let foo { oops }"),
": 1:@if let foo { oops }\n\
: ^ Error in conditional expression:\n\
: 1:@if let foo { oops }\n\
: ^ Expected \'=\'\n"
)
}
#[test]
fn for_in_struct() {
assert_eq!(
template_expression(
b"@for Struct{x, y} in structs { something }"
),
Ok((
&b""[..],
TemplateExpression::ForLoop {
name: "Struct{x, y}".to_string(),
expr: "structs".to_string(),
body: vec![TemplateExpression::text(" something ")],
}
))
)
}
#[test]
fn for_missing_in() {
// TODO The second part of this message isn't really helpful.
assert_eq!(
expression_error(b"@for what ever { hello }"),
": 1:@for what ever { hello }\n\
: ^ Expected \"in\"\n"
)
}
fn expression_error(input: &[u8]) -> String {
let mut buf = Vec::new();
if let Err(error) = template_expression(input) {
show_errors(&mut buf, input, &error, ":");
}
String::from_utf8(buf).unwrap()
}
}

View File

@ -0,0 +1,103 @@
//! The module containing your generated template code will also
//! contain everything from here.
//!
//! The name `ructe::templates` should never be used. Instead, you
//! should use the module templates created when compiling your
//! templates.
//! If you include the generated `templates.rs` in your `main.rs` (or
//! `lib.rs` in a library crate), this module will be
//! `crate::templates`.
mod utils;
pub use self::utils::*;
#[cfg(feature = "mime03")]
use mime::Mime;
#[cfg(feature = "mime02")]
/// Documentation mock. The real Mime type comes from the `mime` crate.
pub type Mime = u8; // mock
/// A static file has a name (so its url can be recognized) and the
/// actual file contents.
///
/// The content-type (mime type) of the file is available as a
/// static field when building ructe with the `mime03` feature or
/// as the return value of a method when building ructe with the
/// `mime02` feature (in `mime` version 0.2.x, a Mime cannot be
/// defined as a part of a const static value.
pub struct StaticFile {
/// The actual static file contents.
pub content: &'static [u8],
/// The file name as used in a url, including a short (48 bits
/// as 8 base64 characters) hash of the content, to enable
/// long-time caching of static resourses in the clients.
pub name: &'static str,
/// The Mime type of this static file, as defined in the mime
/// crate version 0.3.x.
#[cfg(feature = "mime03")]
pub mime: &'static Mime,
}
impl StaticFile {
/// Get the mime type of this static file.
///
/// Currently, this method parses a (static) string every time.
/// A future release of `mime` may support statically created
/// `Mime` structs, which will make this nicer.
#[allow(unused)]
#[cfg(feature = "mime02")]
pub fn mime(&self) -> Mime {
unimplemented!()
}
}
#[test]
fn encoded() {
let mut buf = Vec::new();
"a < b\0\n".to_html(&mut buf).unwrap();
assert_eq!(b"a &lt; b\0\n", &buf[..]);
let mut buf = Vec::new();
"'b".to_html(&mut buf).unwrap();
assert_eq!(b"&#39;b", &buf[..]);
let mut buf = Vec::new();
"xxxxx>&".to_html(&mut buf).unwrap();
assert_eq!(b"xxxxx&gt;&amp;", &buf[..]);
}
#[test]
fn encoded_empty() {
let mut buf = Vec::new();
"".to_html(&mut buf).unwrap();
"".to_html(&mut buf).unwrap();
"".to_html(&mut buf).unwrap();
assert_eq!(b"", &buf[..]);
}
#[test]
fn double_encoded() {
let mut buf = Vec::new();
"&amp;".to_html(&mut buf).unwrap();
"&lt;".to_html(&mut buf).unwrap();
assert_eq!(b"&amp;amp;&amp;lt;", &buf[..]);
}
#[test]
fn encoded_only() {
let mut buf = Vec::new();
"&&&&&&&&&&&&&&&&".to_html(&mut buf).unwrap();
assert_eq!(b"&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;&amp;" as &[u8], &buf[..]);
let mut buf = Vec::new();
"''''''''''''''".to_html(&mut buf).unwrap();
assert_eq!(b"&#39;&#39;&#39;&#39;&#39;&#39;&#39;&#39;&#39;&#39;&#39;&#39;&#39;&#39;" as &[u8], &buf[..]);
}
#[test]
fn raw_html() {
let mut buf = Vec::new();
Html("a<b>c</b>").to_html(&mut buf).unwrap();
assert_eq!(b"a<b>c</b>", &buf[..]);
}

View File

@ -0,0 +1,109 @@
use std::fmt::Display;
use std::io::{self, Write};
/// This trait should be implemented for any value that can be the
/// result of an expression in a template.
///
/// This trait decides how to format the given object as html.
/// There exists a default implementation for any `T: Display` that
/// formats the value using Display and then html-encodes the result.
pub trait ToHtml {
/// Write self to `out`, which is in html representation.
fn to_html(&self, out: &mut dyn Write) -> io::Result<()>;
/// Write the HTML represention of this value to a buffer.
///
/// This can be used for testing, and for short-cutting situations
/// with complex ownership, since the resulting buffer gets owned
/// by the caller.
///
/// # Examples
/// ```ignore
/// # fn main() -> std::io::Result<()> {
/// # use ructe::templates;
/// use templates::ToHtml;
/// assert_eq!(17.to_buffer()?, "17");
/// assert_eq!("a < b".to_buffer()?, "a &lt; b");
/// # Ok(())
/// # }
/// ```
fn to_buffer(&self) -> io::Result<HtmlBuffer> {
let mut buf = Vec::new();
self.to_html(&mut buf)?;
Ok(HtmlBuffer { buf })
}
}
/// Return type for [`ToHtml::to_buffer`].
///
/// An opaque heap-allocated buffer containing a rendered HTML snippet.
pub struct HtmlBuffer {
#[doc(hidden)]
buf: Vec<u8>,
}
impl std::fmt::Debug for HtmlBuffer {
fn fmt(&self, out: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(out, "HtmlBuffer({:?})", String::from_utf8_lossy(&self.buf))
}
}
impl ToHtml for HtmlBuffer {
fn to_html(&self, out: &mut dyn Write) -> io::Result<()> {
out.write_all(&self.buf)
}
}
impl AsRef<[u8]> for HtmlBuffer {
fn as_ref(&self) -> &[u8] {
&self.buf
}
}
impl PartialEq<&[u8]> for HtmlBuffer {
fn eq(&self, other: &&[u8]) -> bool {
&self.buf == other
}
}
impl PartialEq<&str> for HtmlBuffer {
fn eq(&self, other: &&str) -> bool {
let other: &[u8] = other.as_ref();
self.buf == other
}
}
/// Wrapper object for data that should be outputted as raw html
/// (objects that may contain markup).
#[allow(dead_code)]
pub struct Html<T>(pub T);
impl<T: Display> ToHtml for Html<T> {
#[inline]
fn to_html(&self, out: &mut dyn Write) -> io::Result<()> {
write!(out, "{}", self.0)
}
}
impl<T: Display> ToHtml for T {
#[inline]
fn to_html(&self, out: &mut dyn Write) -> io::Result<()> {
write!(ToHtmlEscapingWriter(out), "{self}")
}
}
struct ToHtmlEscapingWriter<'a>(&'a mut dyn Write);
impl<'a> Write for ToHtmlEscapingWriter<'a> {
#[inline]
// This takes advantage of the fact that `write` doesn't have to write everything,
// and the call will be retried with the rest of the data
// (it is a part of `write_all`'s loop or similar.)
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
self.0.write(data)
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
}

View File

@ -0,0 +1,119 @@
use mime::TEXT_HTML_UTF_8;
use std::error::Error;
use std::io;
use warp::http::{header::CONTENT_TYPE, response};
use warp::{reject::Reject, reply::Response, Reply};
/// Extension trait for [`response::Builder`] to simplify template rendering.
///
/// Render a template to a buffer, and use that buffer to complete a
/// `Response` from the builder. Also set the content type of the
/// response to `TEXT_HTML_UTF_8`.
///
/// # Examples
///
/// Give a template `page`, that takes two arguments other than the
/// `Write` buffer, this will use the variables `title` and `body` and
/// render the template to a response.
///
/// ```
/// # use std::io::{self, Write};
/// use warp::http::Response;
/// use ructe::templates::RenderRucte;
///
/// # fn page(o: &mut Write, _: u8, _: u8) -> io::Result<()> { Ok(()) }
/// # let (title, body) = (47, 11);
/// // ... at the end of a handler:
/// Response::builder().html(|o| page(o, title, body))
/// # ;
/// ```
///
/// Other builder methods can be called before calling the `html` method.
/// Here is an example that sets a cookie in the Response.
///
/// ```
/// # use std::io::{self, Write};
/// # use warp::http::{header::SET_COOKIE, Response};
/// # use ructe::templates::RenderRucte;
/// # fn page(o: &mut Write, _: u8, _: u8) -> io::Result<()> { Ok(()) }
/// # let (title, body, value) = (47, 11, 14);
/// Response::builder()
/// .header(SET_COOKIE, format!("FOO={}, SameSite=Strict; HttpOnly", value))
/// .html(|o| page(o, title, body))
/// # ;
/// ```
///
/// Note that the `.html` method _finalizes_ the builder, that is, on
/// success it returns a [`Response`] rather than a [`response::Builder`].
pub trait RenderRucte {
/// Render a template on the response builder.
///
/// This is the main function of the trait. Please see the trait documentation.
fn html<F>(self, f: F) -> Result<Response, RenderError>
where
F: FnOnce(&mut Vec<u8>) -> io::Result<()>;
}
impl RenderRucte for response::Builder {
fn html<F>(self, f: F) -> Result<Response, RenderError>
where
F: FnOnce(&mut Vec<u8>) -> io::Result<()>,
{
let mut buf = Vec::new();
f(&mut buf).map_err(RenderError::write)?;
self.header(CONTENT_TYPE, TEXT_HTML_UTF_8.as_ref())
.body(buf.into())
.map_err(RenderError::build)
}
}
/// Error type for [`RenderRucte::html`].
///
/// This type implements [`Error`] for common Rust error handling, but
/// also both [`Reply`] and [`Reject`] to facilitate use in warp filters
/// and handlers.
#[derive(Debug)]
pub struct RenderError {
im: RenderErrorImpl,
}
impl RenderError {
fn build(e: warp::http::Error) -> Self {
RenderError { im: RenderErrorImpl::Build(e) }
}
fn write(e: std::io::Error) -> Self {
RenderError { im: RenderErrorImpl::Write(e) }
}
}
// make variants private
#[derive(Debug)]
enum RenderErrorImpl {
Write(std::io::Error),
Build(warp::http::Error),
}
impl Error for RenderError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match &self.im {
RenderErrorImpl::Write(e) => Some(e),
RenderErrorImpl::Build(e) => Some(e),
}
}
}
impl std::fmt::Display for RenderError {
fn fmt(&self, out: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.im {
RenderErrorImpl::Write(_) => "Failed to write template",
RenderErrorImpl::Build(_) => "Failed to build response",
}.fmt(out)
}
}
impl Reject for RenderError {}
impl Reply for RenderError {
fn into_response(self) -> Response {
Response::new(self.to_string().into())
}
}

View File

@ -5,6 +5,8 @@ pub enum Types {
F32, F64, F32, F64,
I8, I16, I32, I64, I8, I16, I32, I64,
U8, U16, U32, U64, U8, U16, U32, U64,
Array(Box<Types>),
Optional(Box<Types>),
Named(String) Named(String)
} }
@ -17,9 +19,7 @@ pub struct EnumTy {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FieldTy { pub struct FieldTy {
pub name: String, pub name: String,
pub ty: Types, pub ty: Types
pub optional: bool,
pub array: bool
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -32,7 +32,7 @@ pub struct StructTy {
pub struct MethodTy { pub struct MethodTy {
pub name: String, pub name: String,
pub args: Vec<FieldTy>, pub args: Vec<FieldTy>,
pub ret: Option<FieldTy>, pub ret: Option<Types>,
pub ret_stream: bool pub ret_stream: bool
} }
@ -48,14 +48,3 @@ pub struct RPC {
pub structs: Vec<StructTy>, pub structs: Vec<StructTy>,
pub services: Vec<ServiceTy> pub services: Vec<ServiceTy>
} }
impl FieldTy {
pub fn new(ty: Types) -> Self {
Self {
name: String::new(),
ty,
optional: false,
array: false
}
}
}

View File

@ -1,294 +1,55 @@
use std::io::Write;
use itertools::Itertools; use itertools::Itertools;
use crate::data::RPC; use crate::data::RPC;
use super::IndentedWriter;
fn field_ty_to_ty_str(ty: &crate::data::FieldTy, ignore_optional: bool) -> String { pub fn ty_to_str(ty: &crate::data::Types) -> String {
use crate::data::Types; use crate::data::Types;
let inner = match &ty.ty { match &ty {
Types::String => "std::string", Types::String => "std::string".into(),
Types::Bool => "bool", Types::Bool => "bool".into(),
Types::F32 => "std::float_t", Types::F32 => "std::float_t".into(),
Types::F64 => "std::double_t", Types::F64 => "std::double_t".into(),
Types::I8 => "std::int8_t", Types::I8 => "std::int8_t".into(),
Types::I16 => "std::int16_t", Types::I16 => "std::int16_t".into(),
Types::I32 => "std::int32_t", Types::I32 => "std::int32_t".into(),
Types::I64 => "std::int64_t", Types::I64 => "std::int64_t".into(),
Types::U8 => "std::uint8_t", Types::U8 => "std::uint8_t".into(),
Types::U16 => "std::uint16_t", Types::U16 => "std::uint16_t".into(),
Types::U32 => "std::uint32_t", Types::U32 => "std::uint32_t".into(),
Types::U64 => "std::uint64_t", Types::U64 => "std::uint64_t".into(),
Types::Named(name) => name Types::Named(name) => name.into(),
}; Types::Optional(inner) => format!("std::optional<{}>", ty_to_str(inner)),
Types::Array(inner) => format!("std::vector<{}>", ty_to_str(inner))
}
}
if ty.array {
format!("std::vector<{inner}>") pub fn method_args(method: &crate::data::MethodTy) -> String {
} else if ty.optional && !ignore_optional { method.args.iter()
format!("std::optional<{inner}>") .map(|arg| format!("{} &&{}", ty_to_str(&arg.ty), arg.name))
.chain(method.ret_stream.then(|| format!("std::shared_ptr<MRPCStream<{}>>&&", ty_to_str(method.ret.as_ref().unwrap()))))
.join(", ")
}
pub fn method_ret(method: &crate::data::MethodTy) -> String {
if method.ret_stream || method.ret.is_none() {
"void".into()
} else { } else {
inner.to_string() ty_to_str(method.ret.as_ref().unwrap())
} }
} }
fn method_signature(service_name: &String, m: &crate::data::MethodTy) -> String { pub fn call_args(method: &crate::data::MethodTy) -> String {
let mut ret = String::new(); method.args.iter()
.map(|arg| format!("std::move({})", arg.name))
let ret_type = m.ret.as_ref().map_or("void".to_string(), |ty| field_ty_to_ty_str(ty, false)); .chain(method.ret_stream.then_some(String::from("std::move(__stream)")))
.join(", ")
if m.ret_stream {
ret += "void";
} else {
ret += &ret_type;
}
ret += &format!(" {}_{}(", service_name, m.name);
ret += &m.args.iter()
.map(|arg| field_ty_to_ty_str(arg, false))
.chain(m.ret_stream.then(|| format!("std::shared_ptr<MRPCStream<{ret_type}>>")))
.map(|arg| arg + "&&")
.join(", ");
ret += ")";
ret
}
fn output_header(f: &mut IndentedWriter, rpc: &RPC) {
f.f.write_all(
b"#pragma once
#ifndef MRPC_GEN_H
#define MRPC_GEN_H
#include <unordered_map>
#include <memory>
#include <mutex>
#include <iosfwd>
#include <string>
#include <cstdint>
#include <crow.h>
#include <json.hpp>
namespace mrpc {\n").unwrap();
for e in &rpc.enums {
writeln!(f, "enum struct {} : std::uint64_t {{", e.name).unwrap();
if let Some((last, vals)) = e.values.split_last() {
for v in vals {
writeln!(f, "{} = {},", v.0, v.1).unwrap();
}
writeln!(f, "{} = {}", last.0, last.1).unwrap();
}
f.write_all(b"};\n\n").unwrap();
}
for s in &rpc.structs {
writeln!(f, "struct {};", s.name).unwrap();
writeln!(f, "void to_json(nlohmann::json&, const {}&);", s.name).unwrap();
writeln!(f, "void from_json(const nlohmann::json&, {}&);\n", s.name).unwrap();
}
f.f.write_all(b"\n").unwrap();
for s in &rpc.structs {
writeln!(f, "struct {} {{", s.name).unwrap();
for field in &s.fields {
writeln!(f, "{} {};", field_ty_to_ty_str(field, false), field.name).unwrap();
}
f.write_all(b"};\n\n").unwrap();
}
f.f.write_all(
b"struct MRPCStreamImpl {
virtual void close() noexcept final;
virtual void abort() noexcept final;
virtual bool is_open() noexcept final;
protected:
MRPCStreamImpl(crow::websocket::connection *conn, uint64_t id) : conn(conn), id(id) {}
crow::websocket::connection* conn;
std::uint64_t id;
};
template<class T>
struct MRPCStream final : MRPCStreamImpl {
MRPCStream(crow::websocket::connection *conn, uint64_t id) : MRPCStreamImpl(conn, id) {}
bool send(const T &v) noexcept {
if (!conn) return false;
try {
conn->send_text(nlohmann::json{{\"id\", id},{\"data\", v}}.dump());
} catch (const std::exception &_) {
abort();
return false;
}
return true;
}
};
struct MRPCServer {
virtual void install(crow::SimpleApp &app, std::string &&route) final;
private:\n").unwrap();
f.ident = 1;
for service in &rpc.services {
for method in &service.methods {
writeln!(f, "virtual {} = 0;", method_signature(&service.name, method)).unwrap();
}
}
f.f.write_all(b"
virtual void msg_handler(crow::websocket::connection&, const std::string&, bool) final;
std::mutex __streams_mutex;
std::unordered_multimap<crow::websocket::connection*, std::shared_ptr<MRPCStreamImpl>> __streams;
};
}
#endif // MRPC_GEN_H\n").unwrap();
}
fn output_struct_json_stuff(f: &mut IndentedWriter, s: &crate::data::StructTy) {
writeln!(f, "void to_json(json &j, const {} &v) {{", s.name).unwrap();
for field in &s.fields {
if field.optional {
writeln!(f, "json_set_opt(j, \"{0}\", v.{0});", field.name).unwrap();
} else {
writeln!(f, "j[\"{0}\"] = v.{0};", field.name).unwrap();
}
}
f.write_all(b"}\n\n").unwrap();
writeln!(f, "void from_json(const json &j, {} &v) {{", s.name).unwrap();
for field in &s.fields {
if field.optional {
writeln!(f, "v.{0} = json_get_opt<{1}>(j, \"{0}\");", field.name, field_ty_to_ty_str(field, true)).unwrap();
} else {
writeln!(f, "j.at(\"{0}\").get_to(v.{0});", field.name).unwrap();
}
}
f.write_all(b"}\n\n").unwrap();
}
fn output_cpp(f: &mut IndentedWriter, header_name: String, rpc: &RPC) {
writeln!(f, "#include \"{header_name}\"").unwrap();
f.f.write_all(
b"using json = nlohmann::json;
template<class T>
inline std::optional<T> json_get_opt(const json &j, std::string &&k) {
if (j.contains(k) && !j.at(k).is_null())
return j.at(k).get<T>();
else
return std::nullopt;
}
template<class T>
inline void json_set_opt(json &j, std::string &&k, const std::optional<T> &v) {
if (v.has_value())
j[k] = v.value();
else
j[k] = nullptr;
}
namespace mrpc {\n").unwrap();
for s in &rpc.structs {
output_struct_json_stuff(f, s);
}
f.f.write_all(
b"}
template<class T>
void send_msg(crow::websocket::connection &c, uint64_t id, const T &v) {
c.send_text(json{{\"id\", id},{\"data\", v}}.dump());
}
void mrpc::MRPCStreamImpl::close() noexcept {
if (conn != nullptr) {
send_msg(*conn, id, nullptr);
conn = nullptr;
}
}
void mrpc::MRPCStreamImpl::abort() noexcept { conn = nullptr; }
bool mrpc::MRPCStreamImpl::is_open() noexcept { return conn != nullptr; }
void mrpc::MRPCServer::install(crow::SimpleApp &app, std::string &&route) {
app.route_dynamic(std::move(route))
.websocket()
.onclose([&](crow::websocket::connection &c, const std::string&){
std::lock_guard guard{__streams_mutex};
auto range = __streams.equal_range(&c);
for (auto it = range.first; it != range.second; ++it)
it->second->abort();
__streams.erase(&c);
})
.onmessage([this](auto &&a, auto &&b, auto &&c) {
try { msg_handler(a, b, c); }
catch (const std::exception &_) {}
});
}
void mrpc::MRPCServer::msg_handler(crow::websocket::connection &__c, const std::string &__msg, bool) {
json __j = json::parse(__msg);
std::uint64_t __id = __j.at(\"id\");
std::string __service = __j.at(\"service\"), __method = __j.at(\"method\");
try {\n").unwrap();
f.ident = 2;
f.write_all(b"json __data = __j.at(\"data\");\n").unwrap();
let mut first_service = true;
for service in &rpc.services {
if first_service { first_service = false; }
else { f.write_all(b"else ").unwrap(); }
writeln!(f, "if (__service == \"{}\") {{", service.name).unwrap();
let mut first_method = true;
for method in &service.methods {
if first_method { first_method = false; }
else { f.write_all(b"else ").unwrap(); }
writeln!(f, "if (__method == \"{}\") {{", method.name).unwrap();
if method.ret_stream {
writeln!(f, "auto __stream = std::make_shared<MRPCStream<{}>>(&__c, __id);",
field_ty_to_ty_str(method.ret.as_ref().unwrap(), false)).unwrap();
f.write_all(b"{ std::lock_guard guard{__streams_mutex}; __streams.emplace(&__c, __stream); }\n").unwrap();
}
for arg in &method.args {
let ty = field_ty_to_ty_str(&arg, false);
if arg.optional {
writeln!(f, "{0} {1} = json_get_opt<{2}>(__data, \"{1}\");", ty, arg.name, field_ty_to_ty_str(arg, true)).unwrap();
} else {
writeln!(f, "{0} {1} = __data.at(\"{1}\");", ty, arg.name).unwrap();
}
}
let args = method.args.iter()
.map(|arg| format!("std::move({})", arg.name))
.chain(method.ret_stream.then_some(String::from("std::move(__stream)")))
.collect::<Vec<_>>();
if method.ret.is_none() || method.ret_stream {
writeln!(f, "{}_{}({});", service.name, method.name, args.join(", ")).unwrap();
} else {
writeln!(f, "auto __ret = {}_{}({});", service.name, method.name, args.join(", ")).unwrap();
f.write_all(b"send_msg(__c, __id, __ret);\n").unwrap();
}
f.write_all(b"} ").unwrap();
}
f.write_all(b"else { throw std::exception{}; }\n} ").unwrap();
}
f.f.write_all(b"else { throw std::exception{}; }
} catch (const std::exception &_) {
std::cerr << \"Got invalid request \" << __id << \" for \" << __service << \"::\" << __method << std::endl;
}
}\n\n").unwrap();
} }
pub fn gen(file_base_name: &std::path::PathBuf, rpc: &RPC) { pub fn gen(file_base_name: &std::path::PathBuf, rpc: &RPC) {
output_header(&mut IndentedWriter::new(std::fs::File::create(file_base_name.with_extension("h")).unwrap()), rpc); let header_name = file_base_name.with_extension("h");
output_cpp( let header_name = header_name.file_name().unwrap().to_str().unwrap();
&mut IndentedWriter::new(std::fs::File::create(file_base_name.with_extension("cpp")).unwrap()), let h = std::fs::File::create(file_base_name.with_extension("h")).unwrap();
file_base_name.with_extension("h").file_name().unwrap().to_string_lossy().to_string(), let c = std::fs::File::create(file_base_name.with_extension("cpp")).unwrap();
rpc crate::templates::cpp_server_h(h, rpc).unwrap();
); crate::templates::cpp_server_cpp(c, header_name, rpc).unwrap();
} }

View File

@ -1,5 +1,5 @@
mod cpp_s; pub mod cpp_s;
mod ts_c; pub mod ts_c;
#[derive(Debug, Clone, clap::ValueEnum)] #[derive(Debug, Clone, clap::ValueEnum)]
pub enum ServerGenerators { pub enum ServerGenerators {
@ -26,50 +26,3 @@ impl ClientGenerators {
} }
} }
} }
pub struct IndentedWriter {
pub ident: usize,
pub f: std::fs::File,
indent_next: bool
}
impl IndentedWriter {
pub fn new(f: std::fs::File) -> Self {
Self {
ident: 0,
f,
indent_next: false
}
}
}
impl std::io::Write for IndentedWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
for b in buf {
if b == &b'}' && self.ident > 0 {
self.ident -= 1;
}
if b == &b'\n' {
self.indent_next = true;
} else if self.indent_next {
self.indent_next = false;
for _ in 0..self.ident {
self.f.write_all(b" ")?;
}
}
if b == &b'{' {
self.ident += 1;
}
self.f.write_all(&[*b])?;
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
self.f.flush()
}
}

View File

@ -1,157 +1,37 @@
use std::io::Write;
use itertools::Itertools; use itertools::Itertools;
use crate::data::RPC; use crate::data::RPC;
use super::IndentedWriter;
fn output_common(f: &mut IndentedWriter) { pub fn ty_to_str(ty: &crate::data::Types) -> String {
f.f.write_all(
b"interface _WSResponse {
id: number;
data: any;
}
interface _WSWaitingEntry {
ok: (v: any) => void;
err: (reason?: any) => void;
}
export class MRPCConnector {
url: string;
socket: WebSocket;
next_message_id: number;
waiting: { [id: number]: _WSWaitingEntry };
streams: { [id: number]: (v: any) => void };
private open() {
this.socket = new WebSocket(this.url);
this.socket.onmessage = ev => {
const data = JSON.parse(ev.data) as _WSResponse;
if (data.id in this.streams) {
this.streams[data.id](data.data);
if (data.data == null)
delete this.streams[data.id];
} else if (data.id in this.waiting) {
this.waiting[data.id].ok(data.data);
delete this.waiting[data.id];
} else {
console.log(`Got unexpected message: ${data}`);
}
}
this.socket.onerror = () => setTimeout(() => {this.open();}, 2500);
this.socket.onclose = () => setTimeout(() => {this.open();}, 2500);
}
public constructor(url: string) {
this.url = url;
this.next_message_id = 0;
this.waiting = {};
this.streams = {};
this.open();
}\n\n").unwrap();
f.ident = 1;
}
fn field_ty_to_ty_str(ty: &crate::data::FieldTy, with_name: bool) -> String {
use crate::data::Types; use crate::data::Types;
let mut ret = String::new(); match &ty {
if with_name { Types::String => "string".into(),
ret += &ty.name; Types::Bool => "boolean".into(),
if ty.optional {
ret += "?";
}
ret += ": ";
}
ret += match &ty.ty {
Types::String => "string",
Types::Bool => "boolean",
Types::F32 | Types::F64 Types::F32 | Types::F64
|Types::I8 | Types::I16 | Types::I32 | Types::I64 |Types::I8 | Types::I16 | Types::I32 | Types::I64
|Types::U8 | Types::U16 | Types::U32 | Types::U64 => "number", |Types::U8 | Types::U16 | Types::U32 | Types::U64 => "number".into(),
Types::Named(name) => name Types::Named(name) => name.into(),
}; Types::Optional(inner) => format!("({}|null)", ty_to_str(inner)),
if ty.array { Types::Array(inner) => format!("{}[]", ty_to_str(inner))
ret += "[]";
}
ret
}
fn output_services(f: &mut IndentedWriter, rpc: &RPC) {
for service in &rpc.services {
for method in &service.methods {
write!(f, "public {}_{}(", service.name, method.name).unwrap();
f.write_all(method.args.iter()
.map(|arg| field_ty_to_ty_str(arg, true))
.chain(method.ret_stream.then(|| format!("cbk: (v: {}) => void", field_ty_to_ty_str(method.ret.as_ref().unwrap(), false))))
.join(", ")
.as_bytes()
).unwrap();
f.write_all(b")").unwrap();
if let Some(ret) = &method.ret {
if ret.optional {
unimplemented!("Optional return value is current not supported in typescript client");
}
if !method.ret_stream {
write!(f, ": Promise<{}>", field_ty_to_ty_str(ret, false)).unwrap();
}
}
f.write_all(b" {\nconst msg = {id:this.next_message_id++,").unwrap();
write!(f, "service:'{}',method:'{}',data:{{", service.name, method.name).unwrap();
f.write_all(method.args.iter()
.map(|arg| {
if arg.optional {
format!("{0}:{0}||null", arg.name)
} else {
arg.name.clone()
}
})
.join(",")
.as_bytes()
).unwrap();
f.write_all(b"}};\n").unwrap();
if let Some(ret) = &method.ret {
if !method.ret_stream {
writeln!(f, "const p = new Promise<{}>((ok,err) => {{ this.waiting[msg.id] = {{ ok, err }}; }});", field_ty_to_ty_str(ret, false)).unwrap();
} else {
f.write_all(b"this.streams[msg.id] = cbk;\n").unwrap();
}
}
f.write_all(b"this.socket.send(JSON.stringify(msg));\n").unwrap();
if method.ret.is_some() && !method.ret_stream{
f.write_all(b"return p;\n").unwrap();
}
f.write_all(b"}\n\n").unwrap();
}
}
f.write_all(b"}").unwrap();
}
fn output_enums(f: &mut IndentedWriter, rpc: &RPC) {
for e in &rpc.enums {
writeln!(f, "export enum {} {{", e.name).unwrap();
for (k, v) in &e.values {
writeln!(f, "{k} = {v},").unwrap();
}
f.write_all(b"}\n\n").unwrap();
} }
} }
fn output_structs(f: &mut IndentedWriter, rpc: &RPC) { pub fn method_args(method: &crate::data::MethodTy) -> String {
for s in &rpc.structs { method.args.iter()
writeln!(f, "export interface {} {{", s.name).unwrap(); .map(|arg| format!("{}: {}", arg.name, ty_to_str(&arg.ty)))
for field in &s.fields { .chain(method.ret_stream.then(|| format!("__cbk: (v: {}) => void", ty_to_str(method.ret.as_ref().unwrap()))))
writeln!(f, "{};", field_ty_to_ty_str(field, true)).unwrap(); .join(", ")
} }
f.write_all(b"}\n\n").unwrap();
pub fn method_ret(method: &crate::data::MethodTy) -> String {
if method.ret_stream || method.ret.is_none() {
String::new()
} else {
format!(": Promise<{}>", ty_to_str(method.ret.as_ref().unwrap()))
} }
} }
pub fn gen(file_base_name: &std::path::PathBuf, rpc: &RPC) { pub fn gen(file_base_name: &std::path::PathBuf, rpc: &RPC) {
let f = std::fs::File::create(file_base_name.with_extension("ts")).unwrap(); let f = std::fs::File::create(file_base_name.with_extension("ts")).unwrap();
let mut f = IndentedWriter::new(f); crate::templates::typescript_client_ts(f, rpc).unwrap();
let f = &mut f;
output_enums(f, rpc);
output_structs(f, rpc);
output_common(f);
output_services(f, rpc);
} }

View File

@ -1,5 +1,6 @@
mod data; mod data;
mod generators; mod generators;
mod templates;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
@ -53,7 +54,7 @@ fn parse_type_string(ty: String) -> data::Types {
} }
} }
fn parse_type(item: &syn::Type) -> data::FieldTy { fn parse_type(item: &syn::Type) -> data::Types {
match item { match item {
syn::Type::Path(path) => { syn::Type::Path(path) => {
let segments = &path.path.segments; let segments = &path.path.segments;
@ -72,23 +73,17 @@ fn parse_type(item: &syn::Type) -> data::FieldTy {
if args.args.len() != 1 { if args.args.len() != 1 {
emit_error(item.span(), "Expected 1 argument"); emit_error(item.span(), "Expected 1 argument");
} }
let mut ty = match &args.args[0] { let ty = match &args.args[0] {
syn::GenericArgument::Type(v) => parse_type(v), syn::GenericArgument::Type(v) => parse_type(v),
_ => emit_error(item.span(), "Type bracketed arguments expected") _ => emit_error(item.span(), "Type bracketed arguments expected")
}; };
ty.optional = true; data::Types::Optional(ty.into())
ty
} else { } else {
data::FieldTy::new(parse_type_string(segment.ident.to_string())) parse_type_string(segment.ident.to_string())
} }
} }
syn::Type::Slice(slice) => { syn::Type::Slice(slice) => {
let mut ty = parse_type(&slice.elem); data::Types::Array(parse_type(&slice.elem).into())
if ty.array {
emit_error(item.span(), "Double array found");
}
ty.array = true;
ty
} }
_ => emit_error(item.span(), "Unsupported type") _ => emit_error(item.span(), "Unsupported type")
} }
@ -103,15 +98,12 @@ fn parse_struct(item: &syn::ItemStruct) -> data::StructTy {
} }
let name = field.ident.as_ref().unwrap().to_string(); let name = field.ident.as_ref().unwrap().to_string();
let ty = parse_type(&field.ty); let ty = parse_type(&field.ty);
fields.push(data::FieldTy { fields.push(data::FieldTy { name, ty });
name,
..ty
});
} }
data::StructTy { name, fields } data::StructTy { name, fields }
} }
fn try_parse_iterator(ty: &syn::Type) -> Option<data::FieldTy> { fn try_parse_iterator(ty: &syn::Type) -> Option<data::Types> {
if let syn::Type::Path(ty) = ty { if let syn::Type::Path(ty) = ty {
let seg = ty.path.segments.last()?; let seg = ty.path.segments.last()?;
if seg.ident.to_string() == "Iterator" { if seg.ident.to_string() == "Iterator" {
@ -133,12 +125,12 @@ fn parse_method(item: &syn::Signature) -> data::MethodTy {
syn::FnArg::Typed(v) => v, syn::FnArg::Typed(v) => v,
_ => emit_error(arg.span(), "Unsupported argument") _ => emit_error(arg.span(), "Unsupported argument")
}; };
let mut ty = parse_type(&arg.ty); let ty = parse_type(&arg.ty);
ty.name = match &*arg.pat { let name = match &*arg.pat {
syn::Pat::Ident(v) => v.ident.to_string(), syn::Pat::Ident(v) => v.ident.to_string(),
_ => emit_error(arg.span(), "Unsupported argument") _ => emit_error(arg.span(), "Unsupported argument")
}; };
method.args.push(ty); method.args.push(data::FieldTy { name, ty });
} }
match &item.output { match &item.output {

View File

@ -0,0 +1,96 @@
@use crate::data::RPC;
@use crate::generators::cpp_s::*;
@(header_name: &str, rpc: &RPC)
#include "@header_name"
using json = nlohmann::json;
namespace nlohmann @{
template <typename T>
struct adl_serializer<std::optional<T>> @{
static void to_json(json &j, const std::optional<T> &v) @{
if (v.has_value())
j = v.value();
else
j = nullptr;
@}
static void from_json(const json &j, std::optional<T> &v) @{
if (j.is_null())
v.reset();
else
v = j.get<T>();
@}
@};
@}
namespace mrpc @{
@for s in &rpc.structs {
void to_json(nlohmann::json &j, const @s.name &v) @{
@for f in &s.fields { j["@f.name"] = v.@f.name;
}
@}
void from_json(const nlohmann::json &j, @s.name &v) @{
@for f in &s.fields { j.at("@f.name").get_to(v.@f.name);
}
@}
}
template<class T>
void send_msg(crow::websocket::connection &c, uint64_t id, const T &v) @{
c.send_text(json@{@{"id", id@},@{"data", v@}@}.dump());
@}
void mrpc::MRPCStreamImpl::close() noexcept @{
if (conn != nullptr) @{
send_msg(*conn, id, nullptr);
conn = nullptr;
@}
@}
void mrpc::MRPCStreamImpl::abort() noexcept @{ conn = nullptr; @}
bool mrpc::MRPCStreamImpl::is_open() noexcept @{ return conn != nullptr; @}
void mrpc::MRPCServer::install(crow::SimpleApp &app, std::string &&route) @{
app.route_dynamic(std::move(route))
.websocket()
.onclose([&](crow::websocket::connection &c, const std::string&)@{
std::lock_guard guard@{__streams_mutex@};
auto range = __streams.equal_range(&c);
for (auto it = range.first; it != range.second; ++it)
it->second->abort();
__streams.erase(&c);
@})
.onmessage([this](auto &&a, auto &&b, auto &&c) @{
try @{ msg_handler(a, b, c); @}
catch (const std::exception &_) @{@}
@});
@}
void mrpc::MRPCServer::msg_handler(crow::websocket::connection &__c, const std::string &__msg, bool) @{
json __j = json::parse(__msg);
std::uint64_t __id = __j.at("id");
std::string __service = __j.at("service"), __method = __j.at("method");
try @{
json __data = __j.at("data");
@for (si, s) in rpc.services.iter().enumerate() {
@if si > 0 {else }if (__service == "@s.name") @{
@for (mi, m) in s.methods.iter().enumerate() {
@if mi > 0 {else }if (__method == "@m.name") @{
@if m.ret_stream {
auto __stream = std::make_shared<MRPCStream<@ty_to_str(m.ret.as_ref().unwrap())>>(&__c, __id);
@{ std::lock_guard guard@{__streams_mutex@}; __streams.emplace(&__c, __stream); @}
}
@for (name, ty) in m.args.iter().map(|a| (&a.name, ty_to_str(&a.ty))) { @ty @name = __data.at("@name");
}
@if m.ret_stream || m.ret.is_none() {@(s.name)_@(m.name)(@call_args(m));}
else {send_msg(__c, __id, @(s.name)_@(m.name)(@call_args(m)));}
@}
}
else @{ throw std::exception@{@}; @}
@}
}
else @{ throw std::exception@{@}; @}
@} catch (const std::exception &_) @{
std::cerr << "Got invalid request " << __id << " for " << __service << "::" << __method << std::endl;
@}
@}
@}

74
templates/cpp_server.rs.h Normal file
View File

@ -0,0 +1,74 @@
@use itertools::Itertools;
@use crate::data::RPC;
@use crate::generators::cpp_s::*;
@(rpc: &RPC)
#pragma once
#ifndef MRPC_GEN_H
#define MRPC_GEN_H
#include <unordered_map>
#include <memory>
#include <mutex>
#include <iosfwd>
#include <string>
#include <cstdint>
#include <crow.h>
#include <json.hpp>
namespace mrpc @{
@for e in &rpc.enums {
enum struct @e.name : std::uint64_t @{
@e.values.iter().map(|(k,v)| format!("{k} = {v}")).join(",\n ")
@};
}
@for s in &rpc.structs {
struct @s.name;
void to_json(nlohmann::json&, const @s.name&);
void from_json(const nlohmann::json&, @s.name&);
}
@for s in &rpc.structs {
struct @s.name @{
@for f in &s.fields { @ty_to_str(&f.ty) @f.name;
}
@};
}
struct MRPCStreamImpl @{
virtual void close() noexcept final;
virtual void abort() noexcept final;
virtual bool is_open() noexcept final;
protected:
MRPCStreamImpl(crow::websocket::connection *conn, uint64_t id) : conn(conn), id(id) @{@}
crow::websocket::connection* conn;
std::uint64_t id;
@};
template<class T>
struct MRPCStream final : MRPCStreamImpl @{
MRPCStream(crow::websocket::connection *conn, uint64_t id) : MRPCStreamImpl(conn, id) @{@}
bool send(const T &v) noexcept @{
if (!conn) return false;
try @{
conn->send_text(nlohmann::json@{@{"id", id@},@{"data", v@}@}.dump());
@} catch (const std::exception &_) @{
abort();
return false;
@}
return true;
@}
@};
struct MRPCServer @{
virtual void install(crow::SimpleApp &app, std::string &&route) final;
private:
@for s in &rpc.services {@for m in &s.methods { virtual @method_ret(m) @(s.name)_@(m.name)(@method_args(m)) = 0;
}}
virtual void msg_handler(crow::websocket::connection&, const std::string&, bool) final;
std::mutex __streams_mutex;
std::unordered_multimap<crow::websocket::connection*, std::shared_ptr<MRPCStreamImpl>> __streams;
@};
@}
#endif // MRPC_GEN_H

View File

@ -0,0 +1,81 @@
@use itertools::Itertools;
@use crate::data::RPC;
@use crate::generators::ts_c::*;
@(rpc: &RPC)
@for e in &rpc.enums {
export enum @e.name @{
@for (k,v) in &e.values { @k = @v,
}
@}
}
@for s in &rpc.structs {
export interface @s.name @{
@for f in &s.fields { @f.name: @ty_to_str(&f.ty);
}
@}
}
interface _WSResponse @{
id: number;
data: any;
@}
interface _WSWaitingEntry @{
ok: (v: any) => void;
err: (reason?: any) => void;
@}
export class MRPCConnector @{
url: string;
socket: WebSocket;
nmi: number;
waiting: @{ [id: number]: _WSWaitingEntry @};
streams: @{ [id: number]: (v: any) => void @};
private open() @{
this.socket = new WebSocket(this.url);
this.socket.onmessage = ev => @{
const data = JSON.parse(ev.data) as _WSResponse;
if (data.id in this.streams) @{
this.streams[data.id](data.data);
if (data.data == null)
delete this.streams[data.id];
@} else if (data.id in this.waiting) @{
this.waiting[data.id].ok(data.data);
delete this.waiting[data.id];
@} else @{
console.log(`Got unexpected message: $@{data@}`);
@}
@}
this.socket.onerror = () => setTimeout(() => @{this.open();@}, 2500);
this.socket.onclose = () => setTimeout(() => @{this.open();@}, 2500);
@}
private get_prom<T>(id: number): Promise<T> @{
return new Promise<T>((ok, err) => @{ this.waiting[id] = @{ok, err@}; @});
@}
public constructor(url: string) @{
this.url = url;
this.nmi = 0;
this.waiting = @{@};
this.streams = @{@};
this.open();
@}
@for s in &rpc.services { @for m in &s.methods {
public @(s.name)_@(m.name)(@method_args(m))@method_ret(m) @{
const __msg = @{
id: this.nmi++,
service: '@s.name',
method: '@m.name',
data: @{@m.args.iter().map(|a| &a.name).join(",")@}
@};
@if m.ret.is_some() && !m.ret_stream {const __p = this.get_prom<@ty_to_str(m.ret.as_ref().unwrap())>(__msg.id);}
else if m.ret_stream {this.streams[__msg.id] = __cbk;}
this.socket.send(JSON.stringify(__msg));
@if m.ret.is_some() && !m.ret_stream {return __p;}
@}
}}
@}