mirror of
https://github.com/cargo-bins/cargo-binstall.git
synced 2025-04-24 22:30:03 +00:00
Leon template library (#766)
* leon: first implementation * Update crates/leon/src/values.rs Co-authored-by: Jiahao XU <Jiahao_XU@outlook.com> * Workaround orphan rules to make API more intuitive * Fmt * Clippy * Use CoW * Use cow for items too * Test that const construction works * leon: Initial attempt at O(n) parser * leon: finish parser (except escapes) * leon: Improve ergonomics of compile-time templates * Document helpers * leon: Docs tweaks * leon: Use macro to minimise parser tests * leon: add escapes to parser * leon: test escapes preceding keys * leon: add multibyte tests * leon: test escapes following keys * Format * Debug * leon: Don't actually need to keep track of the key * leon: Parse to vec first * leon: there's actually no need for string cows * leon: reorganise and redo macro now that there's no coww * Well that was silly * leon: Adjust text end when pushing * leon: catch unbalanced keys * Add error tests * leon: Catch unfinished escape * Comment out debugging * leon: fuzz * Clippy * leon: Box parse error * leon: &dyn instead of impl * Can't impl FromStr, so rename to parse * Add Vec<> to values * leon: Add benches for ways to supply values * leon: Add bench comparing to std and tt * Fix fuzz * Fmt * Split ParseError and RenderError * Make miette optional * Remove RenderError lifetime * Simplify ParseError type schema * Write concrete Values types instead of generics * Add license files * Reduce criterion deps * Make default a cow * Add a CLI leon tool * Fix tests * Clippy * Disable cli by default * Avoid failing the build when cli is off * Add to ci * Update crates/leon/src/main.rs Co-authored-by: Jiahao XU <Jiahao_XU@outlook.com> * Update crates/leon/Cargo.toml Co-authored-by: Jiahao XU <Jiahao_XU@outlook.com> * Bump version * Error not transparent * Diagnostic can do forwarding * Simplify error type * Expand doc examples * Generic Values for Hash and BTree maps * One more borrowed * Forward implementations * More generics * Add has_keys * Lock stdout in leon tool * No more debug comments in parser * Even more generics * Macros to reduce bench duplication * Further simplify error * Fix leon main * Stable support * Clippy --------- Co-authored-by: Jiahao XU <Jiahao_XU@outlook.com>
This commit is contained in:
parent
daf8cdd010
commit
2227d363f7
20 changed files with 2382 additions and 162 deletions
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
|
@ -48,3 +48,7 @@ updates:
|
||||||
directory: "/crates/detect-targets"
|
directory: "/crates/detect-targets"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
|
- package-ecosystem: "cargo"
|
||||||
|
directory: "/crates/leon"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
|
1
.github/workflows/release-pr.yml
vendored
1
.github/workflows/release-pr.yml
vendored
|
@ -16,6 +16,7 @@ on:
|
||||||
- detect-wasi
|
- detect-wasi
|
||||||
- fs-lock
|
- fs-lock
|
||||||
- normalize-path
|
- normalize-path
|
||||||
|
- leon
|
||||||
version:
|
version:
|
||||||
description: Version to release
|
description: Version to release
|
||||||
required: true
|
required: true
|
||||||
|
|
489
Cargo.lock
generated
489
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -9,6 +9,7 @@ members = [
|
||||||
"crates/fs-lock",
|
"crates/fs-lock",
|
||||||
"crates/normalize-path",
|
"crates/normalize-path",
|
||||||
"crates/detect-targets",
|
"crates/detect-targets",
|
||||||
|
"crates/leon",
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
33
crates/leon/Cargo.toml
Normal file
33
crates/leon/Cargo.toml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
[package]
|
||||||
|
name = "leon"
|
||||||
|
description = "Dead-simple string templating"
|
||||||
|
repository = "https://github.com/cargo-bins/cargo-binstall"
|
||||||
|
documentation = "https://docs.rs/leon"
|
||||||
|
version = "0.0.1"
|
||||||
|
rust-version = "1.61.0"
|
||||||
|
authors = ["Félix Saparelli <felix@passcod.name>"]
|
||||||
|
edition = "2021"
|
||||||
|
license = "Apache-2.0 OR MIT"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
clap = { version = "4.1.4", features = ["derive"], optional = true }
|
||||||
|
miette = { version = "5.5.0", default-features = false, optional = true }
|
||||||
|
thiserror = "1.0.38"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["miette"]
|
||||||
|
cli = ["dep:clap", "miette?/fancy-no-backtrace"]
|
||||||
|
miette = ["dep:miette"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = { version = "0.4.0", default-features = false, features = ["cargo_bench_support"] }
|
||||||
|
serde = { version = "1.0.152", features = ["derive"] }
|
||||||
|
tinytemplate = "1.2.1"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "values"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "others"
|
||||||
|
harness = false
|
176
crates/leon/LICENSE-APACHE
Normal file
176
crates/leon/LICENSE-APACHE
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
23
crates/leon/LICENSE-MIT
Normal file
23
crates/leon/LICENSE-MIT
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
Permission is hereby granted, free of charge, to any
|
||||||
|
person obtaining a copy of this software and associated
|
||||||
|
documentation files (the "Software"), to deal in the
|
||||||
|
Software without restriction, including without
|
||||||
|
limitation the rights to use, copy, modify, merge,
|
||||||
|
publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software
|
||||||
|
is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice
|
||||||
|
shall be included in all copies or substantial portions
|
||||||
|
of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
||||||
|
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
|
||||||
|
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
|
||||||
|
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
DEALINGS IN THE SOFTWARE.
|
89
crates/leon/benches/others.rs
Normal file
89
crates/leon/benches/others.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
use leon::{vals, Template};
|
||||||
|
use serde::Serialize;
|
||||||
|
use tinytemplate::TinyTemplate;
|
||||||
|
|
||||||
|
fn compare_impls(c: &mut Criterion) {
|
||||||
|
const TEMPLATE: &str = "hello {name}! i am {age} years old. my goal is to {goal}. i like: {flower}, {music}, {animal}, {color}, {food}. i'm drinking {drink}";
|
||||||
|
fn replace_fn<'s>(key: &'s str) -> Option<Cow<'s, str>> {
|
||||||
|
Some(Cow::Borrowed(match key {
|
||||||
|
"name" => "marcus",
|
||||||
|
"age" => "42",
|
||||||
|
"goal" => "primary",
|
||||||
|
"flower" => "lotus",
|
||||||
|
"music" => "jazz",
|
||||||
|
"animal" => "cat",
|
||||||
|
"color" => "blue",
|
||||||
|
"food" => "pizza",
|
||||||
|
"drink" => "coffee",
|
||||||
|
_ => return None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Context<'c> {
|
||||||
|
name: &'c str,
|
||||||
|
age: u8,
|
||||||
|
goal: &'c str,
|
||||||
|
flower: &'c str,
|
||||||
|
music: &'c str,
|
||||||
|
animal: &'c str,
|
||||||
|
color: &'c str,
|
||||||
|
food: &'c str,
|
||||||
|
drink: &'c str,
|
||||||
|
}
|
||||||
|
|
||||||
|
let tt_context = Context {
|
||||||
|
name: "marcus",
|
||||||
|
age: 42,
|
||||||
|
goal: "primary",
|
||||||
|
flower: "lotus",
|
||||||
|
music: "jazz",
|
||||||
|
animal: "cat",
|
||||||
|
color: "blue",
|
||||||
|
food: "pizza",
|
||||||
|
drink: "coffee",
|
||||||
|
};
|
||||||
|
|
||||||
|
c.bench_function("leon", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let template = Template::parse(black_box(TEMPLATE)).unwrap();
|
||||||
|
let output = template.render(&vals(replace_fn)).unwrap();
|
||||||
|
black_box(output);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("std, string replaces", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let mut output = black_box(TEMPLATE).to_string();
|
||||||
|
for (key, value) in [
|
||||||
|
("name", "marcus"),
|
||||||
|
("age", "42"),
|
||||||
|
("goal", "primary"),
|
||||||
|
("flower", "lotus"),
|
||||||
|
("music", "jazz"),
|
||||||
|
("animal", "cat"),
|
||||||
|
("color", "blue"),
|
||||||
|
("food", "pizza"),
|
||||||
|
("drink", "coffee"),
|
||||||
|
] {
|
||||||
|
output = output.replace(&format!("{{{}}}", key), value);
|
||||||
|
}
|
||||||
|
black_box(output);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("tiny template", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let mut tt = TinyTemplate::new();
|
||||||
|
tt.add_template("tmp", black_box(TEMPLATE)).unwrap();
|
||||||
|
let output = tt.render("tmp", &tt_context).unwrap();
|
||||||
|
black_box(output);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(compare, compare_impls);
|
||||||
|
criterion_main!(compare);
|
298
crates/leon/benches/values.rs
Normal file
298
crates/leon/benches/values.rs
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
use std::{borrow::Cow, collections::HashMap};
|
||||||
|
|
||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
use leon::{vals, Template, Values, ValuesFn};
|
||||||
|
|
||||||
|
macro_rules! make_values {
|
||||||
|
($($name:expr => $value:expr),*) => {
|
||||||
|
(
|
||||||
|
&[$(($name, $value)),*],
|
||||||
|
{
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
$(
|
||||||
|
map.insert($name, $value);
|
||||||
|
)*
|
||||||
|
map
|
||||||
|
},
|
||||||
|
vals(|key| match key {
|
||||||
|
$(
|
||||||
|
$name => Some(Cow::Borrowed($value)),
|
||||||
|
)*
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn one_replace(c: &mut Criterion) {
|
||||||
|
const TEMPLATE: &str = "Hello, {name}!";
|
||||||
|
|
||||||
|
let (slice, hashmap, vals) = make_values!(
|
||||||
|
"name" => "marcus"
|
||||||
|
);
|
||||||
|
|
||||||
|
inner_bench("one replace", c, TEMPLATE, vals, hashmap, slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn some_replaces(c: &mut Criterion) {
|
||||||
|
const TEMPLATE: &str = "hello {name}! i am {age} years old. my goal is to {goal}. i like: {flower}, {music}, {animal}, {color}, {food}. i'm drinking {drink}";
|
||||||
|
|
||||||
|
let (slice, hashmap, vals) = make_values!(
|
||||||
|
"name" => "marcus",
|
||||||
|
"age" => "42",
|
||||||
|
"goal" => "primary",
|
||||||
|
"flower" => "lotus",
|
||||||
|
"music" => "jazz",
|
||||||
|
"animal" => "cat",
|
||||||
|
"color" => "blue",
|
||||||
|
"food" => "pizza",
|
||||||
|
"drink" => "coffee"
|
||||||
|
);
|
||||||
|
|
||||||
|
inner_bench("some replaces", c, TEMPLATE, vals, hashmap, slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn many_replaces(c: &mut Criterion) {
|
||||||
|
const TEMPLATE: &str = "
|
||||||
|
{artichoke}
|
||||||
|
{aubergine}
|
||||||
|
{asparagus}
|
||||||
|
{broccoflower}
|
||||||
|
{broccoli}
|
||||||
|
{brussels sprouts}
|
||||||
|
{cabbage}
|
||||||
|
{kohlrabi}
|
||||||
|
{Savoy cabbage}
|
||||||
|
{red cabbage}
|
||||||
|
{cauliflower}
|
||||||
|
{celery}
|
||||||
|
{endive}
|
||||||
|
{fiddleheads}
|
||||||
|
{frisee}
|
||||||
|
{fennel}
|
||||||
|
{greens}
|
||||||
|
{arugula}
|
||||||
|
{bok choy}
|
||||||
|
{chard}
|
||||||
|
{collard greens}
|
||||||
|
{kale}
|
||||||
|
{lettuce}
|
||||||
|
{mustard greens}
|
||||||
|
{spinach}
|
||||||
|
{herbs}
|
||||||
|
{anise}
|
||||||
|
{basil}
|
||||||
|
{caraway}
|
||||||
|
{coriander}
|
||||||
|
{chamomile}
|
||||||
|
{daikon}
|
||||||
|
{dill}
|
||||||
|
{squash}
|
||||||
|
{lavender}
|
||||||
|
{cymbopogon}
|
||||||
|
{marjoram}
|
||||||
|
{oregano}
|
||||||
|
{parsley}
|
||||||
|
{rosemary}
|
||||||
|
{thyme}
|
||||||
|
{legumes}
|
||||||
|
{alfalfa sprouts}
|
||||||
|
{azuki beans}
|
||||||
|
{bean sprouts}
|
||||||
|
{black beans}
|
||||||
|
{black-eyed peas}
|
||||||
|
{borlotti bean}
|
||||||
|
{broad beans}
|
||||||
|
{chickpeas, garbanzos, or ceci beans}
|
||||||
|
{green beans}
|
||||||
|
{kidney beans}
|
||||||
|
{lentils}
|
||||||
|
{lima beans or butter bean}
|
||||||
|
{mung beans}
|
||||||
|
{navy beans}
|
||||||
|
{peanuts}
|
||||||
|
{pinto beans}
|
||||||
|
{runner beans}
|
||||||
|
{split peas}
|
||||||
|
{soy beans}
|
||||||
|
{peas}
|
||||||
|
{mange tout or snap peas}
|
||||||
|
{mushrooms}
|
||||||
|
{nettles}
|
||||||
|
{New Zealand spinach}
|
||||||
|
{okra}
|
||||||
|
{onions}
|
||||||
|
{chives}
|
||||||
|
{garlic}
|
||||||
|
{leek}
|
||||||
|
{onion}
|
||||||
|
{shallot}
|
||||||
|
{scallion}
|
||||||
|
{peppers}
|
||||||
|
{bell pepper}
|
||||||
|
{chili pepper}
|
||||||
|
{jalapeño}
|
||||||
|
{habanero}
|
||||||
|
{paprika}
|
||||||
|
{tabasco pepper}
|
||||||
|
{cayenne pepper}
|
||||||
|
{radicchio}
|
||||||
|
{rhubarb}
|
||||||
|
{root vegetables}
|
||||||
|
{beetroot}
|
||||||
|
{beet}
|
||||||
|
{mangelwurzel}
|
||||||
|
{carrot}
|
||||||
|
{celeriac}
|
||||||
|
{corms}
|
||||||
|
{eddoe}
|
||||||
|
{konjac}
|
||||||
|
{taro}
|
||||||
|
{water chestnut}
|
||||||
|
{ginger}
|
||||||
|
{parsnip}
|
||||||
|
{rutabaga}
|
||||||
|
{radish}
|
||||||
|
{wasabi}
|
||||||
|
";
|
||||||
|
|
||||||
|
let (slice, hashmap, vals) = make_values!(
|
||||||
|
"artichoke" => "Abiu",
|
||||||
|
"aubergine" => "Açaí",
|
||||||
|
"asparagus" => "Acerola",
|
||||||
|
"broccoflower" => "Akebi",
|
||||||
|
"broccoli" => "Ackee",
|
||||||
|
"brussels sprouts" => "African Cherry Orange",
|
||||||
|
"cabbage" => "American Mayapple",
|
||||||
|
"kohlrabi" => "Apple",
|
||||||
|
"Savoy cabbage" => "Apricot",
|
||||||
|
"red cabbage" => "Araza",
|
||||||
|
"cauliflower" => "Avocado",
|
||||||
|
"celery" => "Banana",
|
||||||
|
"endive" => "Bilberry",
|
||||||
|
"fiddleheads" => "Blackberry",
|
||||||
|
"frisee" => "Blackcurrant",
|
||||||
|
"fennel" => "Black sapote",
|
||||||
|
"greens" => "Blueberry",
|
||||||
|
"arugula" => "Boysenberry",
|
||||||
|
"bok choy" => "Breadfruit",
|
||||||
|
"chard" => "Buddha's hand",
|
||||||
|
"collard greens" => "Cactus pear",
|
||||||
|
"kale" => "Canistel",
|
||||||
|
"lettuce" => "Cashew",
|
||||||
|
"mustard greens" => "Cempedak",
|
||||||
|
"spinach" => "Cherimoya",
|
||||||
|
"herbs" => "Cherry",
|
||||||
|
"anise" => "Chico fruit",
|
||||||
|
"basil" => "Cloudberry",
|
||||||
|
"caraway" => "Coco de mer",
|
||||||
|
"coriander" => "Coconut",
|
||||||
|
"chamomile" => "Crab apple",
|
||||||
|
"daikon" => "Cranberry",
|
||||||
|
"dill" => "Currant",
|
||||||
|
"squash" => "Damson",
|
||||||
|
"lavender" => "Date",
|
||||||
|
"cymbopogon" => "Dragonfruit",
|
||||||
|
"marjoram" => "Pitaya",
|
||||||
|
"oregano" => "Durian",
|
||||||
|
"parsley" => "Elderberry",
|
||||||
|
"rosemary" => "Feijoa",
|
||||||
|
"thyme" => "Fig",
|
||||||
|
"legumes" => "Finger Lime",
|
||||||
|
"alfalfa sprouts" => "Caviar Lime",
|
||||||
|
"azuki beans" => "Goji berry",
|
||||||
|
"bean sprouts" => "Gooseberry",
|
||||||
|
"black beans" => "Grape",
|
||||||
|
"black-eyed peas" => "Raisin",
|
||||||
|
"borlotti bean" => "Grapefruit",
|
||||||
|
"broad beans" => "Grewia asiatica",
|
||||||
|
"chickpeas, garbanzos, or ceci beans" => "Guava",
|
||||||
|
"green beans" => "Hala Fruit",
|
||||||
|
"kidney beans" => "Honeyberry",
|
||||||
|
"lentils" => "Huckleberry",
|
||||||
|
"lima beans or butter bean" => "Jabuticaba",
|
||||||
|
"mung beans" => "Jackfruit",
|
||||||
|
"navy beans" => "Jambul",
|
||||||
|
"peanuts" => "Japanese plum",
|
||||||
|
"pinto beans" => "Jostaberry",
|
||||||
|
"runner beans" => "Jujube",
|
||||||
|
"split peas" => "Juniper berry",
|
||||||
|
"soy beans" => "Kaffir Lime",
|
||||||
|
"peas" => "Kiwano",
|
||||||
|
"mange tout or snap peas" => "Kiwifruit",
|
||||||
|
"mushrooms" => "Kumquat",
|
||||||
|
"nettles" => "Lemon",
|
||||||
|
"New Zealand spinach" => "Lime",
|
||||||
|
"okra" => "Loganberry",
|
||||||
|
"onions" => "Longan",
|
||||||
|
"chives" => "Loquat",
|
||||||
|
"garlic" => "Lulo",
|
||||||
|
"leek" => "Lychee",
|
||||||
|
"onion" => "Magellan Barberry",
|
||||||
|
"shallot" => "Mamey Apple",
|
||||||
|
"scallion" => "Mamey Sapote",
|
||||||
|
"peppers" => "Mango",
|
||||||
|
"bell pepper" => "Mangosteen",
|
||||||
|
"chili pepper" => "Marionberry",
|
||||||
|
"jalapeño" => "Melon",
|
||||||
|
"habanero" => "Cantaloupe",
|
||||||
|
"paprika" => "Galia melon",
|
||||||
|
"tabasco pepper" => "Honeydew",
|
||||||
|
"cayenne pepper" => "Mouse melon",
|
||||||
|
"radicchio" => "Musk melon",
|
||||||
|
"rhubarb" => "Watermelon",
|
||||||
|
"root vegetables" => "Miracle fruit",
|
||||||
|
"beetroot" => "Momordica fruit",
|
||||||
|
"beet" => "Monstera deliciosa",
|
||||||
|
"mangelwurzel" => "Mulberry",
|
||||||
|
"carrot" => "Nance",
|
||||||
|
"celeriac" => "Nectarine",
|
||||||
|
"corms" => "Orange",
|
||||||
|
"eddoe" => "Blood orange",
|
||||||
|
"konjac" => "Clementine",
|
||||||
|
"taro" => "Mandarine",
|
||||||
|
"water chestnut" => "Tangerine",
|
||||||
|
"ginger" => "Papaya",
|
||||||
|
"parsnip" => "Passionfruit",
|
||||||
|
"rutabaga" => "Pawpaw",
|
||||||
|
"radish" => "Peach",
|
||||||
|
"wasabi" => "Pear"
|
||||||
|
);
|
||||||
|
|
||||||
|
inner_bench("many replaces", c, TEMPLATE, vals, hashmap, slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inner_bench<F>(
|
||||||
|
name: &str,
|
||||||
|
c: &mut Criterion,
|
||||||
|
template_str: &str,
|
||||||
|
vals: ValuesFn<F>,
|
||||||
|
hashmap: HashMap<&str, &str>,
|
||||||
|
slice: &[(&str, &str)],
|
||||||
|
) where
|
||||||
|
F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
|
||||||
|
{
|
||||||
|
c.bench_function(&format!("{name}, fn"), |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let template = Template::parse(black_box(template_str)).unwrap();
|
||||||
|
black_box(template.render(&vals).unwrap());
|
||||||
|
})
|
||||||
|
});
|
||||||
|
c.bench_function(&format!("{name}, hashmap"), |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let template = Template::parse(black_box(template_str)).unwrap();
|
||||||
|
black_box(template.render(&hashmap).unwrap());
|
||||||
|
})
|
||||||
|
});
|
||||||
|
c.bench_function(&format!("{name}, slice"), |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let template = Template::parse(black_box(template_str)).unwrap();
|
||||||
|
black_box(template.render(&slice as &dyn Values).unwrap());
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(one, one_replace);
|
||||||
|
criterion_group!(some, some_replaces);
|
||||||
|
criterion_group!(many, many_replaces);
|
||||||
|
criterion_main!(one, some, many);
|
4
crates/leon/fuzz/.gitignore
vendored
Normal file
4
crates/leon/fuzz/.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
target
|
||||||
|
corpus
|
||||||
|
artifacts
|
||||||
|
coverage
|
150
crates/leon/fuzz/Cargo.lock
generated
Normal file
150
crates/leon/fuzz/Cargo.lock
generated
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
# This file is automatically @generated by Cargo.
|
||||||
|
# It is not intended for manual editing.
|
||||||
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arbitrary"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e90af4de65aa7b293ef2d09daff88501eb254f58edde2e1ac02c82d873eadad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.0.79"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
||||||
|
dependencies = [
|
||||||
|
"jobserver",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leon"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"miette",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leon-fuzz"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"leon",
|
||||||
|
"libfuzzer-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.139"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libfuzzer-sys"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "beb09950ae85a0a94b27676cccf37da5ff13f27076aa1adbc6545dd0d0e1bd4e"
|
||||||
|
dependencies = [
|
||||||
|
"arbitrary",
|
||||||
|
"cc",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miette"
|
||||||
|
version = "5.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4afd9b301defa984bbdbe112b4763e093ed191750a0d914a78c1106b2d0fe703"
|
||||||
|
dependencies = [
|
||||||
|
"miette-derive",
|
||||||
|
"once_cell",
|
||||||
|
"thiserror",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miette-derive"
|
||||||
|
version = "5.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97c2401ab7ac5282ca5c8b518a87635b1a93762b0b90b9990c509888eeccba29"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "once_cell"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.51"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "1.0.107"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror"
|
||||||
|
version = "1.0.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thiserror-impl"
|
||||||
|
version = "1.0.38"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
27
crates/leon/fuzz/Cargo.toml
Normal file
27
crates/leon/fuzz/Cargo.toml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
[package]
|
||||||
|
name = "leon-fuzz"
|
||||||
|
version = "0.0.0"
|
||||||
|
publish = false
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
cargo-fuzz = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
libfuzzer-sys = "0.4"
|
||||||
|
|
||||||
|
[dependencies.leon]
|
||||||
|
path = ".."
|
||||||
|
|
||||||
|
# Prevent this from interfering with workspaces
|
||||||
|
[workspace]
|
||||||
|
members = ["."]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
debug = 1
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "parser"
|
||||||
|
path = "fuzz_targets/parser.rs"
|
||||||
|
test = false
|
||||||
|
doc = false
|
9
crates/leon/fuzz/fuzz_targets/parser.rs
Normal file
9
crates/leon/fuzz/fuzz_targets/parser.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use libfuzzer_sys::fuzz_target;
|
||||||
|
|
||||||
|
fuzz_target!(|data: &[u8]| {
|
||||||
|
if let Ok(s) = std::str::from_utf8(data) {
|
||||||
|
let _ = leon::Template::parse(s);
|
||||||
|
}
|
||||||
|
});
|
85
crates/leon/src/error.rs
Normal file
85
crates/leon/src/error.rs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
|
||||||
|
pub enum RenderError {
|
||||||
|
/// A key was missing from the provided values.
|
||||||
|
#[error("missing key `{0}`")]
|
||||||
|
MissingKey(String),
|
||||||
|
|
||||||
|
/// An I/O error passed through from [`Template::render_into`].
|
||||||
|
#[error("write failed: {0}")]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error that can occur when parsing a template.
|
||||||
|
///
|
||||||
|
/// When the `miette` feature is enabled, this is a rich miette-powered error
|
||||||
|
/// which will highlight the source of the error in the template when output
|
||||||
|
/// (with miette's `fancy` feature). With `miette` disabled, this is opaque.
|
||||||
|
#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
|
||||||
|
#[cfg_attr(feature = "miette", diagnostic(transparent))]
|
||||||
|
#[error(transparent)]
|
||||||
|
pub struct ParseError(Box<InnerParseError>);
|
||||||
|
|
||||||
|
/// The inner (unboxed) type of [`ParseError`].
|
||||||
|
#[derive(Clone, Debug, thiserror::Error, PartialEq, Eq)]
|
||||||
|
#[cfg_attr(feature = "miette", derive(miette::Diagnostic))]
|
||||||
|
#[error("template parse failed")]
|
||||||
|
struct InnerParseError {
|
||||||
|
#[cfg_attr(feature = "miette", source_code)]
|
||||||
|
src: String,
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "miette", label("This bracket is not opening or closing anything. Try removing it, or escaping it with a backslash."))]
|
||||||
|
unbalanced: Option<(usize, usize)>,
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "miette", label("This escape is malformed."))]
|
||||||
|
escape: Option<(usize, usize)>,
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "miette", label("A key cannot be empty."))]
|
||||||
|
key_empty: Option<(usize, usize)>,
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "miette", label("Escapes are not allowed in keys."))]
|
||||||
|
key_escape: Option<(usize, usize)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParseError {
|
||||||
|
pub(crate) fn unbalanced(src: &str, start: usize, end: usize) -> Self {
|
||||||
|
Self(Box::new(InnerParseError {
|
||||||
|
src: String::from(src),
|
||||||
|
unbalanced: Some((start, end.saturating_sub(start) + 1)),
|
||||||
|
escape: None,
|
||||||
|
key_empty: None,
|
||||||
|
key_escape: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn escape(src: &str, start: usize, end: usize) -> Self {
|
||||||
|
Self(Box::new(InnerParseError {
|
||||||
|
src: String::from(src),
|
||||||
|
unbalanced: None,
|
||||||
|
escape: Some((start, end.saturating_sub(start) + 1)),
|
||||||
|
key_empty: None,
|
||||||
|
key_escape: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn key_empty(src: &str, start: usize, end: usize) -> Self {
|
||||||
|
Self(Box::new(InnerParseError {
|
||||||
|
src: String::from(src),
|
||||||
|
unbalanced: None,
|
||||||
|
escape: None,
|
||||||
|
key_empty: Some((start, end.saturating_sub(start) + 1)),
|
||||||
|
key_escape: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn key_escape(src: &str, start: usize, end: usize) -> Self {
|
||||||
|
Self(Box::new(InnerParseError {
|
||||||
|
src: String::from(src),
|
||||||
|
unbalanced: None,
|
||||||
|
escape: None,
|
||||||
|
key_empty: None,
|
||||||
|
key_escape: Some((start, end.saturating_sub(start) + 1)),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
148
crates/leon/src/lib.rs
Normal file
148
crates/leon/src/lib.rs
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
//! Dead-simple string templating.
|
||||||
|
//!
|
||||||
|
//! Leon parses a template string into a list of tokens, and then substitutes
|
||||||
|
//! provided values in. Unlike other templating engines, it is extremely simple:
|
||||||
|
//! it supports no logic, only replaces. It is even simpler than `format!()`,
|
||||||
|
//! albeit with a similar syntax.
|
||||||
|
//!
|
||||||
|
//! # Syntax
|
||||||
|
//!
|
||||||
|
//! ```plain
|
||||||
|
//! it is better to rule { group }
|
||||||
|
//! one can live {adverb} without power
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! A replacement is denoted by `{` and `}`. The contents of the braces, trimmed
|
||||||
|
//! of any whitespace, are the key. Any text outside of braces is left as-is.
|
||||||
|
//!
|
||||||
|
//! To escape a brace, use `\{` or `\}`. To escape a backslash, use `\\`. Keys
|
||||||
|
//! cannot contain escapes.
|
||||||
|
//!
|
||||||
|
//! ```plain
|
||||||
|
//! \{ leon \}
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The above examples, given the values `group = "no one"` and
|
||||||
|
//! `adverb = "honourably"`, would render to:
|
||||||
|
//!
|
||||||
|
//! ```plain
|
||||||
|
//! it is better to rule no one
|
||||||
|
//! one can live honourably without power
|
||||||
|
//! { leon }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # Usage
|
||||||
|
//!
|
||||||
|
//! A template is first parsed to a token list:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use leon::Template;
|
||||||
|
//!
|
||||||
|
//! let template = Template::parse("hello {name}").unwrap();
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The template can be inspected, for example to check if a key is present:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! # use leon::Template;
|
||||||
|
//! #
|
||||||
|
//! # let template = Template::parse("hello {name}").unwrap();
|
||||||
|
//! assert!(template.has_key("name"));
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The template can be rendered to a string:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! # use leon::Template;
|
||||||
|
//! use leon::vals;
|
||||||
|
//! #
|
||||||
|
//! # let template = Template::parse("hello {name}").unwrap();
|
||||||
|
//! assert_eq!(
|
||||||
|
//! template.render(
|
||||||
|
//! &vals(|_key| Some("marcus".into()))
|
||||||
|
//! ).unwrap().as_str(),
|
||||||
|
//! "hello marcus",
|
||||||
|
//! );
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! …or to a writer:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use std::io::Write;
|
||||||
|
//! # use leon::Template;
|
||||||
|
//! use leon::vals;
|
||||||
|
//! #
|
||||||
|
//! # let template = Template::parse("hello {name}").unwrap();
|
||||||
|
//! let mut buf: Vec<u8> = Vec::new();
|
||||||
|
//! template.render_into(
|
||||||
|
//! &mut buf,
|
||||||
|
//! &vals(|key| if key == "name" {
|
||||||
|
//! Some("julius".into())
|
||||||
|
//! } else {
|
||||||
|
//! None
|
||||||
|
//! })
|
||||||
|
//! ).unwrap();
|
||||||
|
//! assert_eq!(buf.as_slice(), b"hello julius");
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! …with a map:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! use std::collections::HashMap;
|
||||||
|
//! # use leon::Template;
|
||||||
|
//! # let template = Template::parse("hello {name}").unwrap();
|
||||||
|
//! let mut values = HashMap::new();
|
||||||
|
//! values.insert("name", "brutus");
|
||||||
|
//! assert_eq!(template.render(&values).unwrap().as_str(), "hello brutus");
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! …or with your own type, if you implement the [`Values`] trait:
|
||||||
|
//!
|
||||||
|
//! ```
|
||||||
|
//! # use leon::Template;
|
||||||
|
//! use std::borrow::Cow;
|
||||||
|
//! use leon::Values;
|
||||||
|
//!
|
||||||
|
//! struct MyMap {
|
||||||
|
//! name: &'static str,
|
||||||
|
//! }
|
||||||
|
//! impl Values for MyMap {
|
||||||
|
//! fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
|
||||||
|
//! if key == "name" {
|
||||||
|
//! Some(self.name.into())
|
||||||
|
//! } else {
|
||||||
|
//! None
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! #
|
||||||
|
//! # let template = Template::parse("hello {name}").unwrap();
|
||||||
|
//! let values = MyMap { name: "pontifex" };
|
||||||
|
//! assert_eq!(template.render(&values).unwrap().as_str(), "hello pontifex");
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # Errors
|
||||||
|
//!
|
||||||
|
//! Leon will return a [`LeonError::InvalidTemplate`] if the template fails to
|
||||||
|
//! parse. This can happen if there are unbalanced braces, or if a key is empty.
|
||||||
|
//!
|
||||||
|
//! Leon will return a [`LeonError::MissingKey`] if a key is missing from keyed
|
||||||
|
//! values passed to [`Template::render()`], unless a default value is provided
|
||||||
|
//! with [`Template.default`].
|
||||||
|
//!
|
||||||
|
//! It will also pass through I/O errors when using [`Template::render_into()`].
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use error::*;
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use template::*;
|
||||||
|
|
||||||
|
#[doc(inline)]
|
||||||
|
pub use values::*;
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod macros;
|
||||||
|
mod parser;
|
||||||
|
mod template;
|
||||||
|
mod values;
|
50
crates/leon/src/macros.rs
Normal file
50
crates/leon/src/macros.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/// Construct a template constant without needing to make an items constant.
|
||||||
|
///
|
||||||
|
/// This is essentially a shorthand for:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use leon::{Item, Template};
|
||||||
|
/// Template::new({
|
||||||
|
/// const ITEMS: &'static [Item<'static>] = &[Item::Text("Hello "), Item::Key("name")];
|
||||||
|
/// ITEMS
|
||||||
|
/// }, Some("world"));
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use leon::Item::*;
|
||||||
|
/// assert_eq!(
|
||||||
|
/// leon::template!(Text("Hello "), Key("name"))
|
||||||
|
/// .render(&[("name", "Магда Нахман")])
|
||||||
|
/// .unwrap(),
|
||||||
|
/// "Hello Магда Нахман",
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// With a default:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use leon::Item::*;
|
||||||
|
/// assert_eq!(
|
||||||
|
/// leon::template!(Text("Hello "), Key("name"); "M. P. T. Acharya")
|
||||||
|
/// .render(&[("city", "Madras")])
|
||||||
|
/// .unwrap(),
|
||||||
|
/// "Hello M. P. T. Acharya",
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! template {
|
||||||
|
($($item:expr),* $(,)?) => {
|
||||||
|
$crate::Template::new({
|
||||||
|
const ITEMS: &'static [$crate::Item<'static>] = &[$($item),*];
|
||||||
|
ITEMS
|
||||||
|
}, ::core::option::Option::None)
|
||||||
|
};
|
||||||
|
($($item:expr),* $(,)? ; $default:expr) => {
|
||||||
|
$crate::Template::new({
|
||||||
|
const ITEMS: &'static [$crate::Item<'static>] = &[$($item),*];
|
||||||
|
ITEMS
|
||||||
|
}, ::core::option::Option::Some($default))
|
||||||
|
};
|
||||||
|
}
|
61
crates/leon/src/main.rs
Normal file
61
crates/leon/src/main.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
#[cfg(feature = "cli")]
|
||||||
|
fn main() -> miette::Result<()> {
|
||||||
|
use std::{collections::HashMap, error::Error, io::stdout};
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use leon::Template;
|
||||||
|
|
||||||
|
/// Render a Leon template with the given values.
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about, long_about = None)]
|
||||||
|
struct Args {
|
||||||
|
/// Leon template
|
||||||
|
template: String,
|
||||||
|
|
||||||
|
/// Default to use for missing keys
|
||||||
|
#[arg(long)]
|
||||||
|
default: Option<String>,
|
||||||
|
|
||||||
|
/// Use values from the environment
|
||||||
|
#[arg(long)]
|
||||||
|
env: bool,
|
||||||
|
|
||||||
|
/// Key-value pairs to use
|
||||||
|
#[arg(short, long, value_parser = parse_key_val::<String, String>)]
|
||||||
|
values: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a single key-value pair
|
||||||
|
fn parse_key_val<T, U>(s: &str) -> Result<(T, U), Box<dyn Error + Send + Sync + 'static>>
|
||||||
|
where
|
||||||
|
T: std::str::FromStr,
|
||||||
|
T::Err: Error + Send + Sync + 'static,
|
||||||
|
U: std::str::FromStr,
|
||||||
|
U::Err: Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
let (k, v) = s
|
||||||
|
.split_once('=')
|
||||||
|
.ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
|
||||||
|
Ok((k.parse()?, v.parse()?))
|
||||||
|
}
|
||||||
|
|
||||||
|
let args = Args::parse();
|
||||||
|
let mut values: HashMap<String, String> = HashMap::from_iter(args.values);
|
||||||
|
if args.env {
|
||||||
|
for (key, value) in std::env::vars() {
|
||||||
|
values.entry(key).or_insert(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let template = args.template;
|
||||||
|
let mut template = Template::parse(&template)?;
|
||||||
|
if let Some(default) = &args.default {
|
||||||
|
template.set_default(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
template.render_into(&mut stdout().lock(), &values)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "cli"))]
|
||||||
|
fn main() {}
|
565
crates/leon/src/parser.rs
Normal file
565
crates/leon/src/parser.rs
Normal file
|
@ -0,0 +1,565 @@
|
||||||
|
use std::mem::replace;
|
||||||
|
|
||||||
|
use crate::{Item, ParseError, Template};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum Token {
|
||||||
|
Text {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
BracePair {
|
||||||
|
start: usize,
|
||||||
|
key_seen: bool,
|
||||||
|
end: usize,
|
||||||
|
},
|
||||||
|
Escape {
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
ch: Option<char>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Token {
|
||||||
|
fn start_text(pos: usize, ch: char) -> Self {
|
||||||
|
Self::Text {
|
||||||
|
start: pos,
|
||||||
|
end: pos + ch.len_utf8(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_text_single(pos: usize) -> Self {
|
||||||
|
Self::Text {
|
||||||
|
start: pos,
|
||||||
|
end: pos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_brace_pair(pos: usize, ch: char) -> Self {
|
||||||
|
Self::BracePair {
|
||||||
|
start: pos,
|
||||||
|
key_seen: false,
|
||||||
|
end: pos + ch.len_utf8(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_escape(pos: usize, ch: char) -> Self {
|
||||||
|
Self::Escape {
|
||||||
|
start: pos,
|
||||||
|
end: pos + ch.len_utf8(),
|
||||||
|
ch: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self, source_len: usize) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Text { start, end } => {
|
||||||
|
*start >= source_len || *end >= source_len || *start > *end
|
||||||
|
}
|
||||||
|
Self::BracePair {
|
||||||
|
start,
|
||||||
|
key_seen,
|
||||||
|
end,
|
||||||
|
} => !key_seen || *start >= source_len || *end >= source_len || *start > *end,
|
||||||
|
Self::Escape { start, end, .. } => {
|
||||||
|
*start >= source_len || *end >= source_len || *start > *end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Text { start, .. }
|
||||||
|
| Self::BracePair { start, .. }
|
||||||
|
| Self::Escape { start, .. } => *start,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::Text { end, .. } | Self::BracePair { end, .. } | Self::Escape { end, .. } => *end,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_end(&mut self, pos: usize) {
|
||||||
|
match self {
|
||||||
|
Self::Text { end, .. } | Self::BracePair { end, .. } | Self::Escape { end, .. } => {
|
||||||
|
*end = pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> Template<'s> {
|
||||||
|
pub(crate) fn parse_items(s: &'s str) -> Result<Vec<Item<'s>>, ParseError> {
|
||||||
|
let source_len = s.len();
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
|
||||||
|
let mut current = Token::start_text(0, '\0');
|
||||||
|
|
||||||
|
for (pos, chara) in s.char_indices() {
|
||||||
|
match (&mut current, chara) {
|
||||||
|
(tok @ (Token::Text { .. } | Token::Escape { ch: Some(_), .. }), ch @ '{') => {
|
||||||
|
if matches!(tok, Token::Text { .. }) && tok.start() == pos {
|
||||||
|
*tok = Token::start_brace_pair(pos, ch);
|
||||||
|
} else {
|
||||||
|
if let Token::Text { end, .. } = tok {
|
||||||
|
*end = pos - 1;
|
||||||
|
}
|
||||||
|
tokens.push(replace(tok, Token::start_brace_pair(pos, ch)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(txt @ Token::Text { .. }, ch @ '\\') => {
|
||||||
|
if txt.is_empty(source_len) || txt.start() == pos {
|
||||||
|
*txt = Token::start_escape(pos, ch);
|
||||||
|
} else {
|
||||||
|
if let Token::Text { end, .. } = txt {
|
||||||
|
*end = pos - 1;
|
||||||
|
}
|
||||||
|
tokens.push(replace(txt, Token::start_escape(pos, ch)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(bp @ Token::BracePair { .. }, '}') => {
|
||||||
|
if let Token::BracePair { end, .. } = bp {
|
||||||
|
*end = pos;
|
||||||
|
} else {
|
||||||
|
unreachable!("bracepair isn't bracepair");
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push(replace(bp, Token::start_text_single(pos + 1)));
|
||||||
|
}
|
||||||
|
(Token::BracePair { start, .. }, '\\') => {
|
||||||
|
return Err(ParseError::key_escape(s, *start, pos));
|
||||||
|
}
|
||||||
|
(Token::BracePair { key_seen, end, .. }, ws) if ws.is_whitespace() => {
|
||||||
|
if *key_seen {
|
||||||
|
*end = pos;
|
||||||
|
} else {
|
||||||
|
// We're in a brace pair, but we're not in the key yet.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(Token::BracePair { key_seen, end, .. }, _) => {
|
||||||
|
*key_seen = true;
|
||||||
|
*end = pos + 1;
|
||||||
|
}
|
||||||
|
(Token::Text { .. }, '}') => {
|
||||||
|
return Err(ParseError::unbalanced(s, pos, pos));
|
||||||
|
}
|
||||||
|
(Token::Text { end, .. }, _) => {
|
||||||
|
*end = pos;
|
||||||
|
}
|
||||||
|
(esc @ Token::Escape { .. }, es @ ('\\' | '{' | '}')) => {
|
||||||
|
if let Token::Escape { start, end, ch, .. } = esc {
|
||||||
|
if ch.is_none() {
|
||||||
|
*end = pos;
|
||||||
|
*ch = Some(es);
|
||||||
|
} else if es == '\\' {
|
||||||
|
// A new escape right after a completed escape.
|
||||||
|
tokens.push(replace(esc, Token::start_escape(pos, es)));
|
||||||
|
} else if es == '{' {
|
||||||
|
// A new brace pair right after a completed escape, should be handled prior to this.
|
||||||
|
unreachable!("escape followed by brace pair, unhandled");
|
||||||
|
} else {
|
||||||
|
// } right after a completed escape, probably unreachable but just in case:
|
||||||
|
return Err(ParseError::key_escape(s, *start, pos));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
unreachable!("escape is not an escape");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(
|
||||||
|
Token::Escape {
|
||||||
|
start, ch: None, ..
|
||||||
|
},
|
||||||
|
_,
|
||||||
|
) => {
|
||||||
|
return Err(ParseError::escape(s, *start, pos));
|
||||||
|
}
|
||||||
|
(Token::Escape { ch: Some(_), .. }, _) => {
|
||||||
|
tokens.push(replace(&mut current, Token::start_text_single(pos)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !current.is_empty(source_len) {
|
||||||
|
if current.end() < source_len - 1 {
|
||||||
|
current.set_end(source_len - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
tokens.push(current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Token::BracePair { start, end, .. } = current {
|
||||||
|
return Err(ParseError::unbalanced(s, start, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Token::Escape {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
ch: None,
|
||||||
|
} = current
|
||||||
|
{
|
||||||
|
return Err(ParseError::escape(s, start, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut items = Vec::with_capacity(tokens.len());
|
||||||
|
for token in tokens {
|
||||||
|
match token {
|
||||||
|
Token::Text { start, end } => {
|
||||||
|
items.push(Item::Text(&s[start..=end]));
|
||||||
|
}
|
||||||
|
Token::BracePair {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
key_seen: false,
|
||||||
|
} => {
|
||||||
|
return Err(ParseError::key_empty(s, start, end));
|
||||||
|
}
|
||||||
|
Token::BracePair {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
key_seen: true,
|
||||||
|
} => {
|
||||||
|
let key = s[start..=end]
|
||||||
|
.trim_matches(|c: char| c.is_whitespace() || c == '{' || c == '}');
|
||||||
|
if key.is_empty() {
|
||||||
|
return Err(ParseError::key_empty(s, start, end));
|
||||||
|
} else {
|
||||||
|
items.push(Item::Key(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Token::Escape {
|
||||||
|
ch: Some(_), end, ..
|
||||||
|
} => {
|
||||||
|
items.push(Item::Text(&s[end..=end]));
|
||||||
|
}
|
||||||
|
Token::Escape {
|
||||||
|
ch: None,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
} => {
|
||||||
|
return Err(ParseError::escape(s, start, end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_valid {
|
||||||
|
use crate::{template, Item::*, Template};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty() {
|
||||||
|
let template = Template::parse("").unwrap();
|
||||||
|
assert_eq!(template, Template::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_keys() {
|
||||||
|
let template = Template::parse("hello world").unwrap();
|
||||||
|
assert_eq!(template, template!(Text("hello world")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn leading_key() {
|
||||||
|
let template = Template::parse("{salutation} world").unwrap();
|
||||||
|
assert_eq!(template, template!(Key("salutation"), Text(" world")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn trailing_key() {
|
||||||
|
let template = Template::parse("hello {name}").unwrap();
|
||||||
|
assert_eq!(template, template!(Text("hello "), Key("name")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn middle_key() {
|
||||||
|
let template = Template::parse("hello {name}!").unwrap();
|
||||||
|
assert_eq!(template, template!(Text("hello "), Key("name"), Text("!")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn middle_text() {
|
||||||
|
let template = Template::parse("{salutation} good {title}").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
template!(Key("salutation"), Text(" good "), Key("title"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiline() {
|
||||||
|
let template = Template::parse(
|
||||||
|
"
|
||||||
|
And if thy native country was { ancient civilisation },
|
||||||
|
What need to slight thee? Came not {hero} thence,
|
||||||
|
Who gave to { country } her books and art of writing?
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
template!(
|
||||||
|
Text("\n And if thy native country was "),
|
||||||
|
Key("ancient civilisation"),
|
||||||
|
Text(",\n What need to slight thee? Came not "),
|
||||||
|
Key("hero"),
|
||||||
|
Text(" thence,\n Who gave to "),
|
||||||
|
Key("country"),
|
||||||
|
Text(" her books and art of writing?\n "),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_no_whitespace() {
|
||||||
|
let template = Template::parse("{word}").unwrap();
|
||||||
|
assert_eq!(template, template!(Key("word")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_leading_whitespace() {
|
||||||
|
let template = Template::parse("{ word}").unwrap();
|
||||||
|
assert_eq!(template, template!(Key("word")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_trailing_whitespace() {
|
||||||
|
let template = Template::parse("{word\n}").unwrap();
|
||||||
|
assert_eq!(template, template!(Key("word")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_both_whitespace() {
|
||||||
|
let template = Template::parse(
|
||||||
|
"{
|
||||||
|
\tword
|
||||||
|
}",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(template, template!(Key("word")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_inner_whitespace() {
|
||||||
|
let template = Template::parse("{ a word }").unwrap();
|
||||||
|
assert_eq!(template, template!(Key("a word")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_left() {
|
||||||
|
let template = Template::parse(r"this \{ single left brace").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
template!(Text("this "), Text("{"), Text(" single left brace"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_right() {
|
||||||
|
let template = Template::parse(r"this \} single right brace").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
template!(Text("this "), Text("}"), Text(" single right brace"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_both() {
|
||||||
|
let template = Template::parse(r"these \{ two \} braces").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
template!(
|
||||||
|
Text("these "),
|
||||||
|
Text("{"),
|
||||||
|
Text(" two "),
|
||||||
|
Text("}"),
|
||||||
|
Text(" braces")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_doubled() {
|
||||||
|
let template = Template::parse(r"these \{\{ four \}\} braces").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
template!(
|
||||||
|
Text("these "),
|
||||||
|
Text("{"),
|
||||||
|
Text("{"),
|
||||||
|
Text(" four "),
|
||||||
|
Text("}"),
|
||||||
|
Text("}"),
|
||||||
|
Text(" braces")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_escape() {
|
||||||
|
let template = Template::parse(r"these \\ backslashes \\\\").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
template!(
|
||||||
|
Text("these "),
|
||||||
|
Text(r"\"),
|
||||||
|
Text(" backslashes "),
|
||||||
|
Text(r"\"),
|
||||||
|
Text(r"\"),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_before_key() {
|
||||||
|
let template = Template::parse(r"\\{ a } \{{ b } \}{ c }").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
template!(
|
||||||
|
Text(r"\"),
|
||||||
|
Key("a"),
|
||||||
|
Text(" "),
|
||||||
|
Text(r"{"),
|
||||||
|
Key("b"),
|
||||||
|
Text(" "),
|
||||||
|
Text(r"}"),
|
||||||
|
Key("c"),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn escape_after_key() {
|
||||||
|
let template = Template::parse(r"{ a }\\ { b }\{ { c }\}").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
template!(
|
||||||
|
Key("a"),
|
||||||
|
Text(r"\"),
|
||||||
|
Text(" "),
|
||||||
|
Key("b"),
|
||||||
|
Text(r"{"),
|
||||||
|
Text(" "),
|
||||||
|
Key("c"),
|
||||||
|
Text(r"}"),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multibyte_texts() {
|
||||||
|
let template = Template::parse("幸徳 {particle} 秋水").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
template!(Text("幸徳 "), Key("particle"), Text(" 秋水"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multibyte_key() {
|
||||||
|
let template = Template::parse("The { 連盟 }").unwrap();
|
||||||
|
assert_eq!(template, template!(Text("The "), Key("連盟")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multibyte_both() {
|
||||||
|
let template = Template::parse("大杉 {栄}").unwrap();
|
||||||
|
assert_eq!(template, template!(Text("大杉 "), Key("栄")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multibyte_whitespace() {
|
||||||
|
let template = Template::parse("岩佐 作{ 太 }郎").unwrap();
|
||||||
|
assert_eq!(template, template!(Text("岩佐 作"), Key("太"), Text("郎")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multibyte_with_escapes() {
|
||||||
|
let template = Template::parse(r"日本\{アナキスト\}連盟").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
template!(
|
||||||
|
Text("日本"),
|
||||||
|
Text(r"{"),
|
||||||
|
Text("アナキスト"),
|
||||||
|
Text(r"}"),
|
||||||
|
Text("連盟")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multibyte_rtl_text() {
|
||||||
|
let template = Template::parse("محمد صايل").unwrap();
|
||||||
|
assert_eq!(template, template!(Text("محمد صايل")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multibyte_rtl_key() {
|
||||||
|
let template = Template::parse("محمد {ريشة}").unwrap();
|
||||||
|
assert_eq!(template, template!(Text("محمد "), Key("ريشة")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test_error {
|
||||||
|
use crate::{ParseError, Template};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_left_half() {
|
||||||
|
let template = Template::parse("{ open").unwrap_err();
|
||||||
|
assert_eq!(template, ParseError::unbalanced("{ open", 0, 6));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_right_half() {
|
||||||
|
let template = Template::parse("open }").unwrap_err();
|
||||||
|
assert_eq!(template, ParseError::unbalanced("open }", 5, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_with_half_escape() {
|
||||||
|
let template = Template::parse(r"this is { not \ allowed }").unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
ParseError::key_escape(r"this is { not \ allowed }", 8, 14)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_with_full_escape() {
|
||||||
|
let template = Template::parse(r"{ not \} allowed }").unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
template,
|
||||||
|
ParseError::key_escape(r"{ not \} allowed }", 0, 6)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_empty() {
|
||||||
|
let template = Template::parse(r"void: {}").unwrap_err();
|
||||||
|
assert_eq!(template, ParseError::key_empty(r"void: {}", 6, 7));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn key_only_whitespace() {
|
||||||
|
let template = Template::parse(r"nothing: { }").unwrap_err();
|
||||||
|
assert_eq!(template, ParseError::key_empty(r"nothing: { }", 9, 11));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_escape() {
|
||||||
|
let template = Template::parse(r"not \a thing").unwrap_err();
|
||||||
|
assert_eq!(template, ParseError::escape(r"not \a thing", 4, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn end_escape() {
|
||||||
|
let template = Template::parse(r"forget me not \").unwrap_err();
|
||||||
|
assert_eq!(template, ParseError::escape(r"forget me not \", 14, 15));
|
||||||
|
}
|
||||||
|
}
|
198
crates/leon/src/template.rs
Normal file
198
crates/leon/src/template.rs
Normal file
|
@ -0,0 +1,198 @@
|
||||||
|
use std::{borrow::Cow, fmt::Display, io::Write, ops::Add};
|
||||||
|
|
||||||
|
use crate::{ParseError, RenderError, Values};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct Template<'s> {
|
||||||
|
pub items: Cow<'s, [Item<'s>]>,
|
||||||
|
pub default: Option<Cow<'s, str>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Item<'s> {
|
||||||
|
Text(&'s str),
|
||||||
|
Key(&'s str),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> Template<'s> {
|
||||||
|
/// Construct a template with the given items and default.
|
||||||
|
///
|
||||||
|
/// You can write a template literal without any help by constructing it directly:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use std::borrow::Cow;
|
||||||
|
/// use leon::{Item, Template};
|
||||||
|
/// const TEMPLATE: Template = Template {
|
||||||
|
/// items: Cow::Borrowed({
|
||||||
|
/// const ITEMS: &'static [Item<'static>] = &[
|
||||||
|
/// Item::Text("Hello"),
|
||||||
|
/// Item::Key("name"),
|
||||||
|
/// ];
|
||||||
|
/// ITEMS
|
||||||
|
/// }),
|
||||||
|
/// default: None,
|
||||||
|
/// };
|
||||||
|
/// assert_eq!(TEMPLATE.render(&[("name", "world")]).unwrap(), "Helloworld");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// As that's a bit verbose, using this function and the enum shorthands can be helpful:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use leon::{Item, Item::*, Template};
|
||||||
|
/// const TEMPLATE: Template = Template::new({
|
||||||
|
/// const ITEMS: &'static [Item<'static>] = &[Text("Hello "), Key("name")];
|
||||||
|
/// ITEMS
|
||||||
|
/// }, Some("world"));
|
||||||
|
///
|
||||||
|
/// assert_eq!(TEMPLATE.render(&[("unrelated", "value")]).unwrap(), "Hello world");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// For an even more ergonomic syntax, see the [`leon::template!`] macro.
|
||||||
|
pub const fn new(items: &'s [Item<'s>], default: Option<&'s str>) -> Template<'s> {
|
||||||
|
Template {
|
||||||
|
items: Cow::Borrowed(items),
|
||||||
|
default: match default {
|
||||||
|
Some(default) => Some(Cow::Borrowed(default)),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a template from a string.
|
||||||
|
///
|
||||||
|
/// # Syntax
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
/// it is better to rule { group }
|
||||||
|
/// one can live {adverb} without power
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// A replacement is denoted by `{` and `}`. The contents of the braces, trimmed
|
||||||
|
/// of any whitespace, are the key. Any text outside of braces is left as-is.
|
||||||
|
///
|
||||||
|
/// To escape a brace, use `\{` or `\}`. To escape a backslash, use `\\`. Keys
|
||||||
|
/// cannot contain escapes.
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
/// \{ leon \}
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The above examples, given the values `group = "no one"` and
|
||||||
|
/// `adverb = "honourably"`, would render to:
|
||||||
|
///
|
||||||
|
/// ```plain
|
||||||
|
/// it is better to rule no one
|
||||||
|
/// one can live honourably without power
|
||||||
|
/// { leon }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use leon::Template;
|
||||||
|
/// let template = Template::parse("hello {name}").unwrap();
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
pub fn parse(s: &'s str) -> Result<Self, ParseError> {
|
||||||
|
Self::parse_items(s).map(|items| Template {
|
||||||
|
items: Cow::Owned(items),
|
||||||
|
default: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_into(
|
||||||
|
&self,
|
||||||
|
writer: &mut dyn Write,
|
||||||
|
values: &dyn Values,
|
||||||
|
) -> Result<(), RenderError> {
|
||||||
|
for token in self.items.as_ref() {
|
||||||
|
match token {
|
||||||
|
Item::Text(text) => writer.write_all(text.as_bytes())?,
|
||||||
|
Item::Key(key) => {
|
||||||
|
if let Some(value) = values.get_value(key) {
|
||||||
|
writer.write_all(value.as_bytes())?;
|
||||||
|
} else if let Some(default) = &self.default {
|
||||||
|
writer.write_all(default.as_bytes())?;
|
||||||
|
} else {
|
||||||
|
return Err(RenderError::MissingKey(key.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&self, values: &dyn Values) -> Result<String, RenderError> {
|
||||||
|
let mut buf = Vec::with_capacity(
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.map(|item| match item {
|
||||||
|
Item::Key(_) => 0,
|
||||||
|
Item::Text(t) => t.len(),
|
||||||
|
})
|
||||||
|
.sum(),
|
||||||
|
);
|
||||||
|
self.render_into(&mut buf, values)?;
|
||||||
|
|
||||||
|
// UNWRAP: We know that the buffer is valid UTF-8 because we only write strings.
|
||||||
|
Ok(String::from_utf8(buf).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_key(&self, key: &str) -> bool {
|
||||||
|
self.has_keys(&[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_keys(&self, keys: &[&str]) -> bool {
|
||||||
|
self.items.iter().any(|token| match token {
|
||||||
|
Item::Key(k) => keys.contains(k),
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keys(&self) -> impl Iterator<Item = &&str> {
|
||||||
|
self.items.iter().filter_map(|token| match token {
|
||||||
|
Item::Key(k) => Some(k),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the default value for this template.
|
||||||
|
pub fn set_default(&mut self, default: &dyn Display) {
|
||||||
|
self.default = Some(Cow::Owned(default.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> Add for Template<'s> {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
fn add(mut self, rhs: Self) -> Self::Output {
|
||||||
|
self.items
|
||||||
|
.to_mut()
|
||||||
|
.extend(rhs.items.as_ref().iter().cloned());
|
||||||
|
if let Some(default) = rhs.default {
|
||||||
|
self.default = Some(default);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::Item::{Key, Text};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn concat_templates() {
|
||||||
|
let t1 = crate::template!(Text("Hello"), Key("name"));
|
||||||
|
let t2 = crate::template!(Text("have a"), Key("adjective"), Text("day"));
|
||||||
|
assert_eq!(
|
||||||
|
t1 + t2,
|
||||||
|
crate::template!(
|
||||||
|
Text("Hello"),
|
||||||
|
Key("name"),
|
||||||
|
Text("have a"),
|
||||||
|
Key("adjective"),
|
||||||
|
Text("day")
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
133
crates/leon/src/values.rs
Normal file
133
crates/leon/src/values.rs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
use std::{
|
||||||
|
borrow::{Borrow, Cow},
|
||||||
|
collections::{BTreeMap, HashMap},
|
||||||
|
hash::{BuildHasher, Hash},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub trait Values {
|
||||||
|
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Values for &T
|
||||||
|
where
|
||||||
|
T: Values,
|
||||||
|
{
|
||||||
|
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
|
||||||
|
T::get_value(self, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V> Values for [(K, V)]
|
||||||
|
where
|
||||||
|
K: AsRef<str>,
|
||||||
|
V: AsRef<str>,
|
||||||
|
{
|
||||||
|
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
|
||||||
|
self.iter().find_map(|(k, v)| {
|
||||||
|
if k.as_ref() == key {
|
||||||
|
Some(Cow::Borrowed(v.as_ref()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V> Values for &[(K, V)]
|
||||||
|
where
|
||||||
|
K: AsRef<str>,
|
||||||
|
V: AsRef<str>,
|
||||||
|
{
|
||||||
|
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
|
||||||
|
(*self).get_value(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V, const N: usize> Values for [(K, V); N]
|
||||||
|
where
|
||||||
|
K: AsRef<str>,
|
||||||
|
V: AsRef<str>,
|
||||||
|
{
|
||||||
|
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
|
||||||
|
self.as_slice().get_value(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V> Values for Vec<(K, V)>
|
||||||
|
where
|
||||||
|
K: AsRef<str>,
|
||||||
|
V: AsRef<str>,
|
||||||
|
{
|
||||||
|
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
|
||||||
|
self.as_slice().get_value(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V, S> Values for HashMap<K, V, S>
|
||||||
|
where
|
||||||
|
K: Borrow<str> + Eq + Hash,
|
||||||
|
V: AsRef<str>,
|
||||||
|
S: BuildHasher,
|
||||||
|
{
|
||||||
|
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
|
||||||
|
self.get(key).map(|v| Cow::Borrowed(v.as_ref()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K, V> Values for BTreeMap<K, V>
|
||||||
|
where
|
||||||
|
K: Borrow<str> + Ord,
|
||||||
|
V: AsRef<str>,
|
||||||
|
{
|
||||||
|
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
|
||||||
|
self.get(key).map(|v| Cow::Borrowed(v.as_ref()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Workaround to allow using functions as [`Values`].
|
||||||
|
///
|
||||||
|
/// As this isn't constructible you'll want to use [`vals()`] instead.
|
||||||
|
pub struct ValuesFn<F>
|
||||||
|
where
|
||||||
|
F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
|
||||||
|
{
|
||||||
|
inner: F,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F> Values for ValuesFn<F>
|
||||||
|
where
|
||||||
|
F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
|
||||||
|
{
|
||||||
|
fn get_value<'s, 'k: 's>(&'s self, key: &'k str) -> Option<Cow<'s, str>> {
|
||||||
|
(self.inner)(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F> From<F> for ValuesFn<F>
|
||||||
|
where
|
||||||
|
F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
|
||||||
|
{
|
||||||
|
fn from(inner: F) -> Self {
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Workaround to allow using functions as [`Values`].
|
||||||
|
///
|
||||||
|
/// Wraps your function so it implements [`Values`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use leon::{Values, vals};
|
||||||
|
///
|
||||||
|
/// fn use_values(_values: impl Values) {}
|
||||||
|
///
|
||||||
|
/// use_values(vals(|_| Some("hello".into())));
|
||||||
|
/// ```
|
||||||
|
pub const fn vals<F>(func: F) -> ValuesFn<F>
|
||||||
|
where
|
||||||
|
F: for<'s> Fn(&'s str) -> Option<Cow<'s, str>> + Send + 'static,
|
||||||
|
{
|
||||||
|
ValuesFn { inner: func }
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue