Replaced code generation with templates
This commit is contained in:
parent
2c1e684e33
commit
3ca0893a94
3
.gitignore
vendored
3
.gitignore
vendored
@ -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/
|
||||||
|
@ -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
3
build.rs
Normal 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
4
ructe-0.17.0/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*~
|
||||||
|
|
||||||
|
target
|
||||||
|
Cargo.lock
|
556
ructe-0.17.0/CHANGELOG.md
Normal file
556
ructe-0.17.0/CHANGELOG.md
Normal 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
38
ructe-0.17.0/Cargo.toml
Normal 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
78
ructe-0.17.0/README.md
Normal 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).
|
3
ructe-0.17.0/rustfmt.toml
Normal file
3
ructe-0.17.0/rustfmt.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
max_width = 78
|
||||||
|
|
||||||
|
reorder_imports = true
|
304
ructe-0.17.0/src/Template_syntax.rs
Normal file
304
ructe-0.17.0/src/Template_syntax.rs
Normal 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>
|
||||||
|
//! })
|
||||||
|
//! ```
|
||||||
|
}
|
275
ructe-0.17.0/src/expression.rs
Normal file
275
ructe-0.17.0/src/expression.rs
Normal 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
454
ructe-0.17.0/src/lib.rs
Normal 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>;
|
70
ructe-0.17.0/src/parseresult.rs
Normal file
70
ructe-0.17.0/src/parseresult.rs
Normal 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();
|
||||||
|
}
|
99
ructe-0.17.0/src/spacelike.rs
Normal file
99
ructe-0.17.0/src/spacelike.rs
Normal 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"[..], ()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
708
ructe-0.17.0/src/staticfiles.rs
Normal file
708
ructe-0.17.0/src/staticfiles.rs
Normal 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",
|
||||||
|
}
|
||||||
|
}
|
253
ructe-0.17.0/src/template.rs
Normal file
253
ructe-0.17.0/src/template.rs
Normal 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""[..], ())));
|
||||||
|
}
|
||||||
|
}
|
662
ructe-0.17.0/src/templateexpression.rs
Normal file
662
ructe-0.17.0/src/templateexpression.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
103
ructe-0.17.0/src/templates/mod.rs
Normal file
103
ructe-0.17.0/src/templates/mod.rs
Normal 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 < b\0\n", &buf[..]);
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
"'b".to_html(&mut buf).unwrap();
|
||||||
|
assert_eq!(b"'b", &buf[..]);
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
"xxxxx>&".to_html(&mut buf).unwrap();
|
||||||
|
assert_eq!(b"xxxxx>&", &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();
|
||||||
|
"&".to_html(&mut buf).unwrap();
|
||||||
|
"<".to_html(&mut buf).unwrap();
|
||||||
|
assert_eq!(b"&amp;&lt;", &buf[..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encoded_only() {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
"&&&&&&&&&&&&&&&&".to_html(&mut buf).unwrap();
|
||||||
|
assert_eq!(b"&&&&&&&&&&&&&&&&" as &[u8], &buf[..]);
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
"''''''''''''''".to_html(&mut buf).unwrap();
|
||||||
|
assert_eq!(b"''''''''''''''" 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[..]);
|
||||||
|
}
|
109
ructe-0.17.0/src/templates/utils.rs
Normal file
109
ructe-0.17.0/src/templates/utils.rs
Normal 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 < 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()
|
||||||
|
}
|
||||||
|
}
|
119
ructe-0.17.0/src/templates/utils_warp03.rs
Normal file
119
ructe-0.17.0/src/templates/utils_warp03.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
19
src/data.rs
19
src/data.rs
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
30
src/main.rs
30
src/main.rs
@ -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 {
|
||||||
|
96
templates/cpp_server.rs.cpp
Normal file
96
templates/cpp_server.rs.cpp
Normal 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
74
templates/cpp_server.rs.h
Normal 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
|
81
templates/typescript_client.rs.ts
Normal file
81
templates/typescript_client.rs.ts
Normal 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;}
|
||||||
|
@}
|
||||||
|
}}
|
||||||
|
@}
|
Loading…
Reference in New Issue
Block a user