Moved entry into separate file.

This commit is contained in:
Jeff Baskin 2023-03-22 22:41:40 -04:00
parent 1f36848450
commit 40aae3ef5a
5 changed files with 392 additions and 393 deletions

View File

@ -1,4 +1,4 @@
use super::{DBError, ErrorCode, FileData, SessionData, Store};
use super::{DBError, FileData, SessionData};
use std::slice;
#[derive(Clone)]
@ -16,21 +16,21 @@ impl FileData<Self> for Database {
output
}
fn from_bytes(data: &mut slice::Iter<u8>) -> Result<Self, DBError> {
fn from_bytes(_data: &mut slice::Iter<u8>) -> Result<Self, DBError> {
Ok(Self {})
}
}
impl SessionData for Database {
fn add(&mut self, key: &str, value: &str, data: &str) -> Result<Vec<String>, DBError> {
fn add(&mut self, _key: &str, _value: &str, _data: &str) -> Result<Vec<String>, DBError> {
Ok(Vec::new())
}
fn eq(&self, key: &str, value: &str) -> Result<Vec<String>, DBError> {
fn eq(&self, _key: &str, _value: &str) -> Result<Vec<String>, DBError> {
Ok(Vec::new())
}
fn list(&self, keys: Vec<&str>) -> Result<Vec<String>, DBError> {
fn list(&self, _keys: Vec<&str>) -> Result<Vec<String>, DBError> {
Ok(Vec::new())
}
}

375
src/morethantext/entry.rs Normal file
View File

@ -0,0 +1,375 @@
use async_std::{fs::{read, remove_file, write}, path::PathBuf};
use super::{DataType, DBError, ErrorCode, FileData, SessionData};
use std::{cell::Cell, time::{Duration, Instant}};
pub struct Entry {
data: DataType,
filename: PathBuf,
last_used: Cell<Instant>,
}
impl Entry {
pub async fn new<P>(filename: P, data: DataType) -> Result<Self, DBError>
where
P: Into<PathBuf>,
{
let pathbuf = filename.into();
if pathbuf.as_path().exists().await {
return Err(DBError::from_code(ErrorCode::EntryExists(pathbuf)));
} else {
match write(&pathbuf, data.to_bytes()).await {
Ok(_) => (),
Err(err) => {
let mut error = DBError::from_code(ErrorCode::EntryWriteFailure(pathbuf));
error.add_source(err);
return Err(error);
}
}
}
Ok(Self {
data: data,
filename: pathbuf,
last_used: Cell::new(Instant::now()),
})
}
pub async fn get<P>(filename: P) -> Result<Self, DBError>
where
P: Into<PathBuf>,
{
let pathbuf = filename.into();
let content = match read(&pathbuf).await {
Ok(text) => text,
Err(err) => {
let mut error = DBError::from_code(ErrorCode::EntryReadFailure(pathbuf));
error.add_source(err);
return Err(error);
}
};
let data = match DataType::from_bytes(&mut content.iter()) {
Ok(raw) => raw,
Err(err) => {
let mut error = DBError::from_code(ErrorCode::EntryReadFailure(pathbuf));
error.add_source(err);
return Err(error);
}
};
Ok(Self {
data: data,
filename: pathbuf,
last_used: Cell::new(Instant::now()),
})
}
pub fn elapsed(&self) -> Duration {
self.last_used.get().elapsed()
}
pub fn data(&self) -> DataType {
self.last_used.set(Instant::now());
self.data.clone()
}
async fn update(&mut self, data: DataType) -> Result<(), DBError> {
self.last_used.set(Instant::now());
match write(&self.filename, data.to_bytes()).await {
Ok(_) => (),
Err(err) => {
let mut error =
DBError::from_code(ErrorCode::EntryWriteFailure(self.filename.clone()));
error.add_source(err);
return Err(error);
}
};
self.data = data;
Ok(())
}
async fn remove(&self) -> Result<(), DBError> {
match remove_file(&self.filename).await {
Ok(_) => Ok(()),
Err(err) => {
let mut error =
DBError::from_code(ErrorCode::EntryDeleteFailure(self.filename.clone()));
error.add_source(err);
Err(error)
}
}
}
}
#[cfg(test)]
mod entry {
use super::*;
use std::error::Error;
use tempfile::tempdir;
#[async_std::test]
async fn get_elapsed_time() {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("count");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data).await.unwrap();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been now."
);
item.last_used
.set(Instant::now() - Duration::from_secs(500));
assert!(
Duration::from_secs(499) < item.elapsed(),
"The duration should have increased."
);
}
#[async_std::test]
async fn create() {
let dir = tempdir().unwrap();
let mut data = DataType::new("store").unwrap();
data.add("database", "roger", "moore").unwrap();
let filepath = dir.path().join("wiliam");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been now."
);
let output = item.data();
assert_eq!(
data.list(["database"].to_vec()).unwrap(),
output.list(["database"].to_vec()).unwrap()
);
assert!(filepath.is_file(), "Should have created the entry file.");
let content = read(&filepath).await.unwrap();
assert_eq!(content, data.to_bytes());
}
#[async_std::test]
async fn create_errors_on_bad_files() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("bad").join("path");
let filename = filepath.to_str().unwrap();
match Entry::new(filename.to_string(), data).await {
Ok(_) => Err(DBError::new("bad file names should raise an error")),
Err(err) => match err.code {
ErrorCode::EntryWriteFailure(_) => {
assert!(err.source().is_some(), "Must include the source error.");
assert!(err
.source()
.unwrap()
.to_string()
.contains("could not write to file"));
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn create_does_not_over_writes() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let id = "wicked";
let file = dir.path().join(id);
let filename = file.to_str().unwrap();
write(&file, b"previous").await.unwrap();
let data = DataType::new("store").unwrap();
match Entry::new(filename.to_string(), data).await {
Ok(_) => {
return Err(DBError::new(
"Should produce an error for an existing Entry",
))
}
Err(err) => match err.code {
ErrorCode::EntryExists(_) => Ok(()),
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn get_updates_last_used() {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("holder");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data).await.unwrap();
item.last_used
.set(Instant::now() - Duration::from_secs(300));
item.data();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been reset."
);
}
#[async_std::test]
async fn update_entry() {
let dir = tempdir().unwrap();
let mut data = DataType::new("store").unwrap();
let filepath = dir.path().join("changing");
let filename = filepath.to_str().unwrap();
let mut item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
item.last_used
.set(Instant::now() - Duration::from_secs(500));
data.add("database", "new", "stuff").unwrap();
item.update(data.clone()).await.unwrap();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been reset."
);
let output = item.data();
assert_eq!(
data.list(["database"].to_vec()).unwrap(),
output.list(["database"].to_vec()).unwrap()
);
let content = read(&filepath).await.unwrap();
assert_eq!(content, data.to_bytes());
}
#[async_std::test]
async fn update_write_errors() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("changing");
let filename = filepath.to_str().unwrap();
let mut item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
drop(dir);
match item.update(data).await {
Ok(_) => Err(DBError::new("file writes should return an error")),
Err(err) => match err.code {
ErrorCode::EntryWriteFailure(_) => {
assert!(err.source().is_some(), "Must include the source error.");
assert!(err
.source()
.unwrap()
.to_string()
.contains("could not write to file"));
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn retrieve() {
let dir = tempdir().unwrap();
let mut data = DataType::new("store").unwrap();
data.add("database", "something_old", "3.14159").unwrap();
let filepath = dir.path().join("existing");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
let output = Entry::get(filename).await.unwrap();
assert_eq!(
output.data().list(["database"].to_vec()).unwrap(),
data.list(["database"].to_vec()).unwrap()
);
assert_eq!(output.filename.to_str().unwrap(), filename);
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been reset."
);
}
#[async_std::test]
async fn retrieve_file_missing() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let filepath = dir.path().join("justnotthere");
let filename = filepath.to_str().unwrap();
match Entry::get(filename).await {
Ok(_) => Err(DBError::new("should have returned an error")),
Err(err) => match err.code {
ErrorCode::EntryReadFailure(_) => {
assert!(err.source().is_some(), "Error should have a source.");
assert!(
err.source()
.unwrap()
.to_string()
.contains("could not read file"),
"Source Error Message: {}",
err.source().unwrap().to_string()
);
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn retrieve_corrupt_file() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let filepath = dir.path().join("garbage");
let filename = filepath.to_str().unwrap();
write(&filepath, b"jhsdfghlsdf").await.unwrap();
match Entry::get(filename).await {
Ok(_) => Err(DBError::new("should have returned an error")),
Err(err) => match err.code {
ErrorCode::EntryReadFailure(_) => {
assert!(err.source().is_some(), "Error should have a source.");
assert!(
err.source().unwrap().to_string().contains("corrupt file"),
"Source Error Message: {}",
err.source().unwrap().to_string()
);
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn delete() {
let dir = tempdir().unwrap();
let filepath = dir.path().join("byebye");
let filename = filepath.to_str().unwrap();
let data = DataType::new("store").unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
item.remove().await.unwrap();
assert!(!filepath.exists(), "Entry file should be removed.");
}
#[async_std::test]
async fn delete_bad_file() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let filepath = dir.path().join("itsnotthere");
let filename = filepath.to_str().unwrap();
let data = DataType::new("store").unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
remove_file(filename).await.unwrap();
match item.remove().await {
Ok(_) => Err(DBError::new("should have produced an error")),
Err(err) => match err.code {
ErrorCode::EntryDeleteFailure(_) => {
assert!(err.source().is_some(), "Error should have a source.");
assert!(
err.source()
.unwrap()
.to_string()
.contains("could not remove file"),
"Source Error Message: {}",
err.source().unwrap().to_string()
);
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
}

View File

@ -158,11 +158,11 @@ mod codes {
use super::*;
use async_std::path::PathBuf;
const items: [&str; 2] = ["first", "second"];
const ITEMS: [&str; 2] = ["first", "second"];
fn create_path_buffer() -> Vec<PathBuf> {
let mut output = Vec::new();
for item in items {
for item in ITEMS {
let mut path = PathBuf::new();
path.push("thepath");
path.push(item);
@ -173,7 +173,7 @@ mod codes {
#[test]
fn undefined_display() {
for item in items {
for item in ITEMS {
let err = ErrorCode::Undefined(item.to_string());
assert_eq!(err.to_string(), item);
}
@ -181,7 +181,7 @@ mod codes {
#[test]
fn incorrect_data_type() {
for item in items {
for item in ITEMS {
let err = ErrorCode::DataTypeIncorrect(item.to_string());
assert_eq!(
err.to_string(),

View File

@ -1,19 +1,16 @@
mod database;
mod entry;
mod error;
mod store;
use async_std::{
fs::{read, remove_file, write},
path::{Path, PathBuf},
fs::{write},
path::PathBuf,
};
use database::Database;
use entry::Entry;
use error::{DBError, ErrorCode};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use std::{
cell::Cell,
slice, str,
time::{Duration, Instant},
};
use std::{slice, str};
use store::Store;
const ENTRY: &str = "EntryPoint";
@ -30,7 +27,7 @@ trait SessionData {
}
#[derive(Clone)]
enum DataType {
pub enum DataType {
DBMap(Store),
TableMap(Database),
}
@ -80,7 +77,7 @@ impl FileData<Self> for DataType {
output.push(0);
match self {
DataType::DBMap(store) => output.append(&mut store.to_bytes()),
DataType::TableMap(db) => (),
DataType::TableMap(_) => (),
}
output
}
@ -113,102 +110,6 @@ impl FileData<Self> for DataType {
}
}
struct Entry {
data: DataType,
filename: PathBuf,
last_used: Cell<Instant>,
}
impl Entry {
async fn new<P>(filename: P, data: DataType) -> Result<Self, DBError>
where
P: Into<PathBuf>,
{
let pathbuf = filename.into();
if pathbuf.as_path().exists().await {
return Err(DBError::from_code(ErrorCode::EntryExists(pathbuf)));
} else {
match write(&pathbuf, data.to_bytes()).await {
Ok(_) => (),
Err(err) => {
let mut error = DBError::from_code(ErrorCode::EntryWriteFailure(pathbuf));
error.add_source(err);
return Err(error);
}
}
}
Ok(Self {
data: data,
filename: pathbuf,
last_used: Cell::new(Instant::now()),
})
}
async fn get<P>(filename: P) -> Result<Self, DBError>
where
P: Into<PathBuf>,
{
let pathbuf = filename.into();
let content = match read(&pathbuf).await {
Ok(text) => text,
Err(err) => {
let mut error = DBError::from_code(ErrorCode::EntryReadFailure(pathbuf));
error.add_source(err);
return Err(error);
}
};
let data = match DataType::from_bytes(&mut content.iter()) {
Ok(raw) => raw,
Err(err) => {
let mut error = DBError::from_code(ErrorCode::EntryReadFailure(pathbuf));
error.add_source(err);
return Err(error);
}
};
Ok(Self {
data: data,
filename: pathbuf,
last_used: Cell::new(Instant::now()),
})
}
fn elapsed(&self) -> Duration {
self.last_used.get().elapsed()
}
fn data(&self) -> DataType {
self.last_used.set(Instant::now());
self.data.clone()
}
async fn update(&mut self, data: DataType) -> Result<(), DBError> {
self.last_used.set(Instant::now());
match write(&self.filename, data.to_bytes()).await {
Ok(_) => (),
Err(err) => {
let mut error =
DBError::from_code(ErrorCode::EntryWriteFailure(self.filename.clone()));
error.add_source(err);
return Err(error);
}
};
self.data = data;
Ok(())
}
async fn remove(&self) -> Result<(), DBError> {
match remove_file(&self.filename).await {
Ok(_) => Ok(()),
Err(err) => {
let mut error =
DBError::from_code(ErrorCode::EntryDeleteFailure(self.filename.clone()));
error.add_source(err);
Err(error)
}
}
}
}
#[derive(Clone)]
pub struct MoreThanText;
@ -395,282 +296,6 @@ mod datatype_file {
}
}
#[cfg(test)]
mod entry {
use super::*;
use std::error::Error;
use tempfile::tempdir;
#[async_std::test]
async fn get_elapsed_time() {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("count");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data).await.unwrap();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been now."
);
item.last_used
.set(Instant::now() - Duration::from_secs(500));
assert!(
Duration::from_secs(499) < item.elapsed(),
"The duration should have increased."
);
}
#[async_std::test]
async fn create() {
let dir = tempdir().unwrap();
let mut data = DataType::new("store").unwrap();
data.add("database", "roger", "moore").unwrap();
let filepath = dir.path().join("wiliam");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been now."
);
let output = item.data();
assert_eq!(
data.list(["database"].to_vec()).unwrap(),
output.list(["database"].to_vec()).unwrap()
);
assert!(filepath.is_file(), "Should have created the entry file.");
let content = read(&filepath).await.unwrap();
assert_eq!(content, data.to_bytes());
}
#[async_std::test]
async fn create_errors_on_bad_files() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("bad").join("path");
let filename = filepath.to_str().unwrap();
match Entry::new(filename.to_string(), data).await {
Ok(_) => Err(DBError::new("bad file names should raise an error")),
Err(err) => match err.code {
ErrorCode::EntryWriteFailure(_) => {
assert!(err.source().is_some(), "Must include the source error.");
assert!(err
.source()
.unwrap()
.to_string()
.contains("could not write to file"));
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn create_does_not_over_writes() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let id = "wicked";
let file = dir.path().join(id);
let filename = file.to_str().unwrap();
write(&file, b"previous").await.unwrap();
let data = DataType::new("store").unwrap();
match Entry::new(filename.to_string(), data).await {
Ok(_) => {
return Err(DBError::new(
"Should produce an error for an existing Entry",
))
}
Err(err) => match err.code {
ErrorCode::EntryExists(_) => Ok(()),
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn get_updates_last_used() {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("holder");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data).await.unwrap();
item.last_used
.set(Instant::now() - Duration::from_secs(300));
item.data();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been reset."
);
}
#[async_std::test]
async fn update_entry() {
let dir = tempdir().unwrap();
let mut data = DataType::new("store").unwrap();
let filepath = dir.path().join("changing");
let filename = filepath.to_str().unwrap();
let mut item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
item.last_used
.set(Instant::now() - Duration::from_secs(500));
data.add("database", "new", "stuff").unwrap();
item.update(data.clone()).await.unwrap();
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been reset."
);
let output = item.data();
assert_eq!(
data.list(["database"].to_vec()).unwrap(),
output.list(["database"].to_vec()).unwrap()
);
let content = read(&filepath).await.unwrap();
assert_eq!(content, data.to_bytes());
}
#[async_std::test]
async fn update_write_errors() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let data = DataType::new("store").unwrap();
let filepath = dir.path().join("changing");
let filename = filepath.to_str().unwrap();
let mut item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
drop(dir);
match item.update(data).await {
Ok(_) => Err(DBError::new("file writes should return an error")),
Err(err) => match err.code {
ErrorCode::EntryWriteFailure(_) => {
assert!(err.source().is_some(), "Must include the source error.");
assert!(err
.source()
.unwrap()
.to_string()
.contains("could not write to file"));
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn retrieve() {
let dir = tempdir().unwrap();
let mut data = DataType::new("store").unwrap();
data.add("database", "something_old", "3.14159").unwrap();
let filepath = dir.path().join("existing");
let filename = filepath.to_str().unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
let output = Entry::get(filename).await.unwrap();
assert_eq!(
output.data().list(["database"].to_vec()).unwrap(),
data.list(["database"].to_vec()).unwrap()
);
assert_eq!(output.filename.to_str().unwrap(), filename);
assert!(
Duration::from_secs(1) > item.elapsed(),
"last_used should have been reset."
);
}
#[async_std::test]
async fn retrieve_file_missing() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let filepath = dir.path().join("justnotthere");
let filename = filepath.to_str().unwrap();
match Entry::get(filename).await {
Ok(_) => Err(DBError::new("should have returned an error")),
Err(err) => match err.code {
ErrorCode::EntryReadFailure(_) => {
assert!(err.source().is_some(), "Error should have a source.");
assert!(
err.source()
.unwrap()
.to_string()
.contains("could not read file"),
"Source Error Message: {}",
err.source().unwrap().to_string()
);
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn retrieve_corrupt_file() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let filepath = dir.path().join("garbage");
let filename = filepath.to_str().unwrap();
write(&filepath, b"jhsdfghlsdf").await.unwrap();
match Entry::get(filename).await {
Ok(_) => Err(DBError::new("should have returned an error")),
Err(err) => match err.code {
ErrorCode::EntryReadFailure(_) => {
assert!(err.source().is_some(), "Error should have a source.");
assert!(
err.source().unwrap().to_string().contains("corrupt file"),
"Source Error Message: {}",
err.source().unwrap().to_string()
);
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
#[async_std::test]
async fn delete() {
let dir = tempdir().unwrap();
let filepath = dir.path().join("byebye");
let filename = filepath.to_str().unwrap();
let data = DataType::new("store").unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
item.remove().await.unwrap();
assert!(!filepath.exists(), "Entry file should be removed.");
}
#[async_std::test]
async fn delete_bad_file() -> Result<(), DBError> {
let dir = tempdir().unwrap();
let filepath = dir.path().join("itsnotthere");
let filename = filepath.to_str().unwrap();
let data = DataType::new("store").unwrap();
let item = Entry::new(filename.to_string(), data.clone())
.await
.unwrap();
remove_file(filename).await.unwrap();
match item.remove().await {
Ok(_) => Err(DBError::new("should have produced an error")),
Err(err) => match err.code {
ErrorCode::EntryDeleteFailure(_) => {
assert!(err.source().is_some(), "Error should have a source.");
assert!(
err.source()
.unwrap()
.to_string()
.contains("could not remove file"),
"Source Error Message: {}",
err.source().unwrap().to_string()
);
Ok(())
}
_ => Err(DBError::new("incorrect error code")),
},
}
}
}
#[cfg(test)]
mod cache {
use super::*;
@ -724,7 +349,7 @@ mod cache {
Entry::new(dir.path().join(ENTRY), data.clone())
.await
.unwrap();
let cache = MoreThanText::new(dir.path()).await.unwrap();
MoreThanText::new(dir.path()).await.unwrap();
}
#[async_std::test]

View File

@ -40,7 +40,6 @@ impl FileData<Self> for Store {
let mut id: Vec<u8> = Vec::new();
let mut get_id = false;
let mut letter: u8;
let err_msg = "file corruption";
loop {
match data.next() {
Some(a) => letter = a.clone(),