Skip to content

Commit

Permalink
refactor(controllers): Unify event flow for controllers (#41)
Browse files Browse the repository at this point in the history
  • Loading branch information
mertwole committed Aug 25, 2024
1 parent b8da79c commit a51af74
Show file tree
Hide file tree
Showing 16 changed files with 537 additions and 129 deletions.
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

0 comments on commit a51af74

Please sign in to comment.