Functional templating
This commit is contained in:
135
Cargo.lock
generated
135
Cargo.lock
generated
@@ -177,6 +177,12 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "equivalent"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eyre"
|
name = "eyre"
|
||||||
version = "0.6.8"
|
version = "0.6.8"
|
||||||
@@ -193,6 +199,12 @@ version = "0.28.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
@@ -205,6 +217,16 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indexmap"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897"
|
||||||
|
dependencies = [
|
||||||
|
"equivalent",
|
||||||
|
"hashbrown",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@@ -352,6 +374,26 @@ version = "0.7.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rprompt"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42ac661565a32b2874200835012773dd9f97e9113a0ab78b69792480a8d9b28e"
|
||||||
|
dependencies = [
|
||||||
|
"rtoolbox",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rtoolbox"
|
||||||
|
version = "0.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
@@ -365,9 +407,42 @@ dependencies = [
|
|||||||
"clap",
|
"clap",
|
||||||
"color-eyre",
|
"color-eyre",
|
||||||
"eyre",
|
"eyre",
|
||||||
|
"rprompt",
|
||||||
|
"serde",
|
||||||
|
"toml",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"upon",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.188"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.188"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -412,6 +487,40 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing"
|
name = "tracing"
|
||||||
version = "0.1.37"
|
version = "0.1.37"
|
||||||
@@ -490,6 +599,23 @@ version = "1.0.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "upon"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21a9260fe394dfd8ab204a8eab40f88eb9a331bb852147d24fc0aff6b30daa02"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"unicode-ident",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@@ -589,3 +715,12 @@ name = "windows_x86_64_msvc"
|
|||||||
version = "0.48.5"
|
version = "0.48.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.5.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "037711d82167854aff2018dfd193aa0fef5370f456732f0d5a0c59b0f1b4b907"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ edition = "2021"
|
|||||||
clap = { version = "4.4.6", features = ["derive"] }
|
clap = { version = "4.4.6", features = ["derive"] }
|
||||||
color-eyre = "0.6.2"
|
color-eyre = "0.6.2"
|
||||||
eyre = "0.6.8"
|
eyre = "0.6.8"
|
||||||
|
rprompt = "2.0.2"
|
||||||
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
|
toml = "0.8.2"
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||||
|
upon = "0.7.1"
|
||||||
|
|||||||
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Sablono
|
||||||
|
Generates directories based on templates.
|
||||||
|
Templates are defined by their manifest, which is a TOML file.
|
||||||
1
example-tmpl/abc
Normal file
1
example-tmpl/abc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
abc 123 {{ name }}
|
||||||
9
example-tmpl/manifest.toml
Normal file
9
example-tmpl/manifest.toml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[parameters]
|
||||||
|
|
||||||
|
[files."abc"]
|
||||||
|
|
||||||
|
[files."qwe"]
|
||||||
|
src = "abc"
|
||||||
|
transforms = []
|
||||||
|
|
||||||
|
|
||||||
84
src/context.rs
Normal file
84
src/context.rs
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::manifest::{FileDefinition, Manifest, Source, Transform, Params};
|
||||||
|
use eyre::Result;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Context {
|
||||||
|
manifest: Manifest,
|
||||||
|
root: Source,
|
||||||
|
params: Params,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Context {
|
||||||
|
#[instrument(ret)]
|
||||||
|
pub fn load(source: Source, params: Params) -> Result<Self> {
|
||||||
|
let params = Params::load_user()?.merge(params);
|
||||||
|
Ok(Context {
|
||||||
|
manifest: load_manifest(&source)?,
|
||||||
|
root: source.parent().ok_or_else(|| eyre::eyre!("Expect manifest to have a parent"))?,
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
pub fn generate(self, dest: PathBuf) -> Result<()> {
|
||||||
|
std::fs::create_dir(&dest)?;
|
||||||
|
|
||||||
|
for (path, def) in &self.manifest.files {
|
||||||
|
self.generate_file(&dest, path, def)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
fn generate_file(&self, dest: &Path, path: &PathBuf, def: &FileDefinition) -> Result<()> {
|
||||||
|
std::fs::create_dir_all(dest.join(path).parent().expect("`dest` should be an absolute path with a parent"))?;
|
||||||
|
let mut input = self
|
||||||
|
.root
|
||||||
|
.join(
|
||||||
|
&def.src
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| Source::Path(path.to_owned())),
|
||||||
|
)
|
||||||
|
.load()?;
|
||||||
|
|
||||||
|
for transform in &def.transforms {
|
||||||
|
input = self.run_transform(transform, input)?;
|
||||||
|
}
|
||||||
|
std::fs::write(dest.join(path), input)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
fn run_transform(&self, transform: &Transform, input: Vec<u8>) -> Result<Vec<u8>> {
|
||||||
|
match transform.r#type.as_str() {
|
||||||
|
"template" => run_template(&transform.args, &self.params, input),
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(ret)]
|
||||||
|
fn load_manifest(src: &Source) -> Result<Manifest> {
|
||||||
|
Ok(toml::from_str(std::str::from_utf8(&src.load()?)?)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[instrument(skip(input))]
|
||||||
|
fn run_template(
|
||||||
|
args: &Params,
|
||||||
|
params: &Params,
|
||||||
|
input: Vec<u8>,
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let engine = upon::Engine::new();
|
||||||
|
let context = args.clone().merge(params.clone());
|
||||||
|
Ok(engine
|
||||||
|
.compile(std::str::from_utf8(&input)?)?
|
||||||
|
.render(context)
|
||||||
|
.to_string()?
|
||||||
|
.into_bytes())
|
||||||
|
}
|
||||||
46
src/main.rs
46
src/main.rs
@@ -1,15 +1,34 @@
|
|||||||
use std::io;
|
use std::{
|
||||||
|
io,
|
||||||
|
path::PathBuf,
|
||||||
|
};
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use eyre::Result;
|
use eyre::{ensure, Result};
|
||||||
|
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use tracing_subscriber::{prelude::*, fmt::{self, format::FmtSpan}, EnvFilter};
|
use tracing_subscriber::{
|
||||||
|
fmt::{self, format::FmtSpan},
|
||||||
|
prelude::*,
|
||||||
|
EnvFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::manifest::Params;
|
||||||
|
|
||||||
|
mod context;
|
||||||
|
mod manifest;
|
||||||
|
|
||||||
|
fn split_equals(s: &str) -> Result<(String, String)> {
|
||||||
|
s.split_once('=').ok_or_else(|| eyre::eyre!("Expected =")).map(|(k, v)| (k.to_owned(), v.to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
template: manifest::Source,
|
||||||
|
destination: PathBuf,
|
||||||
|
#[arg(short, long, value_parser = split_equals)]
|
||||||
|
params: Vec<(String, String)>
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
@@ -30,6 +49,23 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[instrument]
|
#[instrument]
|
||||||
fn run(args: Args) -> Result<()> {
|
fn run(
|
||||||
|
Args {
|
||||||
|
template,
|
||||||
|
destination,
|
||||||
|
params
|
||||||
|
}: Args,
|
||||||
|
) -> Result<()> {
|
||||||
|
ensure!(
|
||||||
|
!destination.exists() || (destination.is_dir() && destination.read_dir()?.next().is_none()),
|
||||||
|
"Expected {} to be empty or not exist",
|
||||||
|
destination.display()
|
||||||
|
);
|
||||||
|
// let destination = destination.canonicalize()?;
|
||||||
|
let params = Params(params.into_iter().collect());
|
||||||
|
|
||||||
|
context::Context::load(template, params)?.generate(destination)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
105
src/manifest.rs
Normal file
105
src/manifest.rs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
use eyre::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tracing::instrument;
|
||||||
|
use std::{collections::HashMap, convert::Infallible, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub struct Manifest {
|
||||||
|
pub parameters: HashMap<String, ParameterDefinition>,
|
||||||
|
pub files: HashMap<PathBuf, FileDefinition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub struct ParameterDefinition {
|
||||||
|
pub description: String,
|
||||||
|
pub default: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub struct FileDefinition {
|
||||||
|
pub src: Option<Source>,
|
||||||
|
#[serde(default = "Transform::defaults")]
|
||||||
|
pub transforms: Vec<Transform>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Source {
|
||||||
|
Path(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source {
|
||||||
|
#[instrument(ret)]
|
||||||
|
pub fn join(&self, other: &Source) -> Self {
|
||||||
|
match other {
|
||||||
|
Self::Path(relative) if relative.is_relative() => match self {
|
||||||
|
Self::Path(root) => Self::Path(root.join(relative)),
|
||||||
|
},
|
||||||
|
other => other.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
pub fn parent(&self) -> Option<Self> {
|
||||||
|
match self {
|
||||||
|
Source::Path(path) => Some(Self::Path(path.parent()?.to_owned())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument]
|
||||||
|
pub fn load(&self) -> Result<Vec<u8>> {
|
||||||
|
match self {
|
||||||
|
Self::Path(path) => Ok(std::fs::read(path)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Deserialize<'a> for Source {
|
||||||
|
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'a>,
|
||||||
|
{
|
||||||
|
Ok(String::deserialize(deserializer)?
|
||||||
|
.parse()
|
||||||
|
.unwrap_or_else(|e| match e {}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Source {
|
||||||
|
type Err = Infallible;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Infallible> {
|
||||||
|
Ok(Self::Path(s.to_owned().into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
|
pub struct Transform {
|
||||||
|
pub r#type: String,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub args: Params,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transform {
|
||||||
|
pub fn defaults() -> Vec<Self> {
|
||||||
|
vec![Self {
|
||||||
|
r#type: "template".to_owned(),
|
||||||
|
args: Default::default(),
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Default, Clone, PartialEq)]
|
||||||
|
pub struct Params(pub HashMap<String, String>);
|
||||||
|
|
||||||
|
impl Params {
|
||||||
|
pub fn load_user() -> Result<Self> {
|
||||||
|
Ok(Self(toml::from_str(
|
||||||
|
&std::fs::read_to_string("~/.config/sablono/defaults.toml").unwrap_or_default(),
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(mut self, other: Self) -> Self {
|
||||||
|
self.0.extend(other.0);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user