Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(controllers): Unify event flow for controllers #41

Merged
merged 11 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

resolver = "2"

members = ["./app", "./app/api_proc_macro"]
members = ["./app", "./app/input_mapping/*"]

[workspace.package]
version = "0.1.0"
Expand All @@ -11,6 +11,8 @@ authors = ["mertwole"]

[workspace.dependencies]
api-proc-macro = { path = "./app/api_proc_macro" }
input-mapping-derive = { path = "./app/input_mapping/derive" }
input-mapping-common = { path = "./app/input_mapping/common" }

binance_spot_connector_rust = "1.1.0"
bs58 = "0.5.1"
Expand Down
2 changes: 2 additions & 0 deletions app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ authors.workspace = true

[dependencies]
api-proc-macro.workspace = true
input-mapping-derive.workspace = true
input-mapping-common.workspace = true

binance_spot_connector_rust.workspace = true
bs58.workspace = true
Expand Down
8 changes: 8 additions & 0 deletions app/input_mapping/common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "input-mapping-common"
version.workspace = true
edition.workspace = true
authors.workspace = true

[dependencies]
ratatui.workspace = true
41 changes: 41 additions & 0 deletions app/input_mapping/common/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use ratatui::crossterm::event::{Event, KeyCode};

pub trait InputMappingT: Sized {
fn get_mapping() -> InputMapping;

fn map_event(event: Event) -> Option<Self>;
}

#[derive(Debug)]
pub struct InputMapping {
pub mapping: Vec<MappingEntry>,
}

impl InputMapping {
pub fn merge(mut self, mut other: InputMapping) -> Self {
self.mapping.append(&mut other.mapping);
self
}
}

#[derive(Debug)]
pub struct MappingEntry {
pub key: KeyCode,
pub description: String,
}

pub trait KeyCodeConversions {
fn convert(self) -> KeyCode;
}

impl KeyCodeConversions for char {
fn convert(self) -> KeyCode {
KeyCode::Char(self)
}
}

impl KeyCodeConversions for KeyCode {
fn convert(self) -> KeyCode {
self
}
}
16 changes: 16 additions & 0 deletions app/input_mapping/derive/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "input-mapping-derive"
version.workspace = true
edition.workspace = true
authors.workspace = true

[lib]
proc-macro = true

[dependencies]
input-mapping-common.workspace = true

itertools.workspace = true
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true
183 changes: 183 additions & 0 deletions app/input_mapping/derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
use proc_macro2::{Literal, TokenStream};
use quote::ToTokens;
use syn::{parse_macro_input, Expr, Fields, ItemEnum, Lit, Meta, MetaNameValue, Variant};

#[macro_use]
extern crate quote;
extern crate proc_macro;
extern crate syn;

#[proc_macro_derive(InputMapping, attributes(key, description))]
pub fn derive_mapping(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let item_enum = parse_macro_input!(input as ItemEnum);
let trait_impl = generate_trait_impl(item_enum);

proc_macro::TokenStream::from(trait_impl)
}

// TODO: Add check that mappings don't overlap.
fn generate_trait_impl(item_enum: ItemEnum) -> TokenStream {
let (unit, to_be_flattened): (Vec<_>, Vec<_>) = item_enum
.variants
.into_iter()
.partition(|variant| matches!(variant.fields, Fields::Unit));

let mapping_entries = unit.into_iter().map(generate_mapping_entry);

let mapping_constructors = mapping_entries.clone().map(|entry| {
let key = entry.key;
let description = entry.description;

quote! {
::input_mapping_common::MappingEntry {
key: #key.convert(),
description: (#description).to_string(),
}
}
});

let mapping_matchers = mapping_entries.map(|entry| {
let key = entry.key;
let event = entry.event;

quote! {
::ratatui::crossterm::event::Event::Key(::ratatui::crossterm::event::KeyEvent {
kind: ::ratatui::crossterm::event::KeyEventKind::Press,
code,
..
}) if code == #key.convert() => ::std::option::Option::Some(Self:: #event)
}
});

let get_mapping_flattening = to_be_flattened.iter().map(|variant| {
let field_ty = match &variant.fields {
Fields::Unnamed(fields) => {
let fields = &fields.unnamed;
if fields.len() != 1 {
panic!("Multiple unnamed fields are not supported");
}
&fields.first().expect("fields.len() checked to be = 1").ty
}
Fields::Unit => panic!("Unit fields have been filtered above"),
Fields::Named(_) => panic!("Named variant fields are not supported"),
};

quote! {
.merge(#field_ty ::get_mapping())
}
});

let map_event_flattening = to_be_flattened.iter().map(|variant| {
let field_ty = match &variant.fields {
Fields::Unnamed(fields) => {
let fields = &fields.unnamed;
if fields.len() != 1 {
panic!("Multiple unnamed fields are not supported");
}
&fields.first().expect("fields.len() checked to be = 1").ty
}
Fields::Unit => panic!("Unit fields have been filtered above"),
Fields::Named(_) => panic!("Named variant fields are not supported"),
};

let ident = &variant.ident;

quote! {
.or_else(|| {
#field_ty ::map_event(event).map(Self:: #ident)
})
}
});

let ident = item_enum.ident;

quote! {
impl ::input_mapping_common::InputMappingT for #ident {
fn get_mapping() -> ::input_mapping_common::InputMapping {
use ::input_mapping_common::KeyCodeConversions;

::input_mapping_common::InputMapping {
mapping: vec![
#(#mapping_constructors,)*
]
}

#(#get_mapping_flattening)*
}

fn map_event(event: ::ratatui::crossterm::event::Event) -> ::std::option::Option<Self> {
use ::input_mapping_common::KeyCodeConversions;

match event {
#(#mapping_matchers,)*
_ => None,
}

#(#map_event_flattening)*
}
}
}
}

struct MappingEntry {
key: TokenStream,
description: TokenStream,
event: TokenStream,
}

fn generate_mapping_entry(variant: Variant) -> MappingEntry {
let mut key: Option<TokenStream> = None;
let mut description: Option<TokenStream> = None;

for attr in &variant.attrs {
match &attr.meta {
Meta::NameValue(MetaNameValue { path, value, .. }) => {
if path.is_ident("key") {
if key.is_some() {
panic!("Duplicate definition for attribute: key");
}

key = Some(match value {
Expr::Lit(lit) => match &lit.lit {
Lit::Str(str) => str.value().parse().expect("Invalid expression"),
Lit::Char(char) => char.to_token_stream(),
_ => {
unimplemented!("Values other than string or char are not supported")
}
},
_ => unimplemented!(),
});

//key = Some(value.into_token_stream());
} else if path.is_ident("description") {
if description.is_some() {
panic!("Duplicate definition for attribute: description");
}

description = Some(value.into_token_stream());
}
}
_ => continue,
}
}

let key = key.unwrap_or_else(|| {
let key = variant
.ident
.to_string()
.chars()
.next()
.expect("Non-empty identifier expected")
.to_ascii_lowercase();

Literal::character(key).into_token_stream()
});

let description = description.unwrap_or_else(|| Literal::string("").into_token_stream());

MappingEntry {
key,
description,
event: variant.ident.to_token_stream(),
}
}
11 changes: 11 additions & 0 deletions app/input_mapping/tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "input-mapping-tests"
version.workspace = true
edition.workspace = true
authors.workspace = true

[dependencies]
input-mapping-derive.workspace = true
input-mapping-common.workspace = true

ratatui.workspace = true
50 changes: 50 additions & 0 deletions app/input_mapping/tests/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#![cfg(test)]

use std::collections::HashMap;

use input_mapping_common::InputMappingT;
use input_mapping_derive::InputMapping;
use ratatui::crossterm::event::KeyCode;

#[derive(InputMapping)]
enum TestEnum {
#[key = 'a']
One,

#[description = "test"]
Two,

#[allow(dead_code)]
Nested(Nested),
}

#[derive(InputMapping)]
enum Nested {
#[description = "four_test"]
Four,

#[key = "KeyCode::Up"]
#[description = "up"]
Five,

Six,
}

#[test]
fn test_input_mapping_generated_as_expected() {
let mapping = TestEnum::get_mapping();
let mapping: HashMap<_, _> = mapping
.mapping
.into_iter()
.map(|map| (map.key, map.description))
.collect();

assert_eq!(mapping.len(), 5);
assert_eq!(
mapping.get(&KeyCode::Char('f')),
Some(&"four_test".to_string())
);
assert_eq!(mapping.get(&KeyCode::Char('t')), Some(&"test".to_string()));
assert_eq!(mapping.get(&KeyCode::Char('s')), Some(&"".to_string()));
assert_eq!(mapping.get(&KeyCode::Up), Some(&"up".to_string()));
}
Loading