Skip to content

Commit

Permalink
Merge pull request #7 from NotThatRqd/custom-data
Browse files Browse the repository at this point in the history
1.0.0 update 🎉
  • Loading branch information
NotThatRqd authored Nov 10, 2023
2 parents 9ddd953 + c4de6ad commit 5dd51d1
Show file tree
Hide file tree
Showing 6 changed files with 460 additions and 123 deletions.
9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "btnify"
version = "0.3.0"
version = "1.0.0"
edition = "2021"
description = "Hosts a website with buttons for you so you can focus on what matters!"
license = "MIT"
Expand All @@ -9,7 +9,10 @@ repository = "https://github.com/NotThatRqd/btnify"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
axum = { version = "0.6.20" }
hyper = { version = "0.14.27" }
axum = "0.6.20"
hyper = "0.14.27"
serde = { version = "1.0.189", features = ["derive"] }
serde_json = "1.0.107"

[dev-dependencies]
html-to-string-macro = "0.2.5"
137 changes: 105 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,56 +1,129 @@
# Btnify
Hosts a website with buttons for you so you can focus on what matters!
<div align="center">
<pre>
██████╗ ████████╗███╗ ██╗██╗███████╗██╗ ██╗
██╔══██╗╚══██╔══╝████╗ ██║██║██╔════╝╚██╗ ██╔╝
██████╔╝ ██║ ██╔██╗ ██║██║█████╗ ╚████╔╝
██╔══██╗ ██║ ██║╚██╗██║██║██╔══╝ ╚██╔╝
██████╔╝ ██║ ██║ ╚████║██║██║ ██║
╚═════╝ ╚═╝ ╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝

Btnify is a small libary that lets you host a website with some buttons that will call a function or closure
when clicked. Under the hood, Btnify uses [Axum](https://github.com/tokio-rs/axum). This library is, I must admit,
rather crude, but it works and it's open source! Please leave a pull request with any improvments you have :)
I would appriciate it very much.
---------------------------------------------------
rust library to simplify allowing user input over the web
</pre>

# Examples
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/NotThatRqd/btnify/rust.yml)
[![docs.rs](https://img.shields.io/docsrs/btnify)](https://docs.rs/btnify)

</div>

> Hosts a website with buttons for you so you can focus on what matters!
Btnify is a small library that lets you host a website with some buttons that will call a function or closure
when clicked. Under the hood, Btnify uses [Axum](https://github.com/tokio-rs/axum). This library is pretty simple,
but it works, and it's open source! Please leave a pull request with any improvements you have :) I would appreciate it
very much.

## Installation

Run `cargo add btnify`

or

Add `btnify = "1.0.0"` to your `Cargo.toml`

## How to use

[Docs are here](https://docs.rs/btnify)

## Examples

Hello World

```rust
use btnify::{
bind_server,
button::{Button, ButtonResponse}
};
use btnify::button::{Button, ButtonResponse, ExtraResponse};

fn greet_handler(_: &()) -> ButtonResponse {
ButtonResponse::from("Hello world!")
fn greet_handler() -> ButtonResponse {
ButtonResponse::from("hello world!")
}

let greet_button = Button::new("Greet", greet_handler);
let greet_button: Button<()> = Button::create_basic_button("Greet!", Box::new(greet_handler));
```

Hello World 2.0

```rust
use btnify::button::{Button, ButtonResponse};

let buttons = vec![greet_button];
fn better_greet_handler(responses: Vec<Option<String>>) -> ButtonResponse {
// responses is guaranteed to be the same length as the number of extra prompts
// specified when creating a button
let name = &responses[0];
match name {
Some(name) => format!("Hello, {name}").into(),
None => format!("You didn't provide a name! :(").into()
}
}

// Notice: bind_server is async and you must await it
bind_server(&"0.0.0.0:3000".parse().unwrap(), buttons, ())
.await
.unwrap();
let better_greet_button: Button<()> = Button::create_button_with_prompts(
"Greet 2.0",
Box::new(better_greet_handler),
vec!["What's your name?".to_string()]
);
```

Counter App

```rust
use std::sync::Mutex;
use btnify::{
bind_server,
button::{Button, ButtonResponse}
};
use btnify::bind_server;
use btnify::button::{Button, ButtonResponse, ExtraResponse};

struct Counter {
// must use mutex to be thread-safe
count: Mutex<i32>
}

impl Counter {
fn new() -> Counter {
Counter {
count: Mutex::new(0)
}
}
}

fn count_handler(state: &Counter) -> ButtonResponse {
let mut count = state.count.lock().unwrap();
*count += 1;
format!("The count now is: {count}").into()
let count = state.count.lock().unwrap();
format!("The count is: {count}").into()
}

let count_button = Button::new("Count", count_handler);
fn plus_handler(counter_struct: &Counter, responses: Vec<Option<String>>) -> ButtonResponse {
match &responses[0] {
Some(response_str) => {
if let Ok(amount) = response_str.parse::<i32>() {
let mut count = counter_struct.count.lock().unwrap();
*count += amount;
format!("The count now is: {}", *count).into()
} else {
"You did not provide a number.".into()
}
}
None => "You didn't provide any input.".into(),
}
}

let count_button = Button::create_button_with_state("Counter", Box::new(count_handler));

let plus_button = Button::create_button_with_state_and_prompts(
"add to counter",
Box::new(plus_handler),
vec!["How much do you want to add?".to_string()]
);

let buttons = vec![count_button];
let buttons = [count_button, plus_button];

// Notice: bind_server is async and you must await it
bind_server(&"0.0.0.0:3000".parse().unwrap(), buttons, ())
.await
.unwrap();
// uncomment to run server on localhost:3000
// bind_server(&"0.0.0.0:3000".parse().unwrap(), buttons, Counter::new())
// .await
// .unwrap();
```
39 changes: 39 additions & 0 deletions README.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<div align="center">
<pre>
██████╗ ████████╗███╗ ██╗██╗███████╗██╗ ██╗
██╔══██╗╚══██╔══╝████╗ ██║██║██╔════╝╚██╗ ██╔╝
██████╔╝ ██║ ██╔██╗ ██║██║█████╗ ╚████╔╝
██╔══██╗ ██║ ██║╚██╗██║██║██╔══╝ ╚██╔╝
██████╔╝ ██║ ██║ ╚████║██║██║ ██║
╚═════╝ ╚═╝ ╚═╝ ╚═══╝╚═╝╚═╝ ╚═╝

---------------------------------------------------
rust library to simplify allowing user input over the web
</pre>

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/NotThatRqd/btnify/rust.yml)
[![docs.rs](https://img.shields.io/docsrs/btnify)](https://docs.rs/btnify)

</div>

> Hosts a website with buttons for you so you can focus on what matters!

Btnify is a small library that lets you host a website with some buttons that will call a function or closure
when clicked. Under the hood, Btnify uses [Axum](https://crates.io/crates/axum). This library is pretty simple,
but it works, and it's open source! Please leave a pull request with any improvements you have :) I would appreciate it
very much.

## Installation

Run `cargo add btnify`

or

Add `btnify = "{{version}}"` to your `Cargo.toml`

## How to use

[Docs are here](https://docs.rs/btnify)

{{readme}}
124 changes: 75 additions & 49 deletions src/button.rs
Original file line number Diff line number Diff line change
@@ -1,75 +1,95 @@
use serde::{Deserialize, Serialize};

/// Represents a button you can put on your btnify server.
///
/// `Handler` is a function or closure that takes a reference to a user provided state (`S`) and
/// returns `ButtonResponse`. It will be called whenever this button is pressed.
///
/// # Examples
///
/// ```
/// use btnify::button::{Button, ButtonResponse};
///
/// fn greet_handler(_: &()) -> ButtonResponse {
/// ButtonResponse::from("Hello world!")
/// }
///
/// let greet_button = Button::new("Greet", greet_handler);
/// ```
///
/// ---
///
/// ```
/// use std::sync::Mutex;
/// use btnify::button::{Button, ButtonResponse};
///
/// struct Counter {
/// count: Mutex<i32>
/// }
///
/// fn count_handler(state: &Counter) -> ButtonResponse {
/// let mut count = state.count.lock().unwrap();
/// *count += 1;
/// format!("The count now is: {count}").into()
/// }
///
/// let count_button = Button::new("Count", count_handler);
/// ```
pub struct Button<S: Send + Sync + 'static> {
pub name: String,
pub handler: Box<dyn (Fn(&S) -> ButtonResponse) + Send + Sync>
pub(crate) name: String,
pub(crate) handler: ButtonHandlerVariant<S>,
}

impl<S: Send + Sync + 'static> Button<S> {
/// Creates a new Button struct.
///
/// `Name` is the name of the button that will appear on the website.
///
/// `Handler` is a function or closure that takes a reference to a user provided state (`S`) and
/// returns `ButtonResponse`. It will be called whenever this button is pressed.
pub fn new<T: Send + Sync + Fn(&S) -> ButtonResponse + 'static>(name: &str, handler: T) -> Button<S> {
fn new(name: &str, handler: ButtonHandlerVariant<S>) -> Button<S> {
Button {
name: name.to_string(),
handler: Box::new(handler)
handler,
}
}

/// Creates a Button with the specified name and handler.
pub fn create_basic_button(
name: &str,
handler: Box<dyn Send + Sync + Fn() -> ButtonResponse>,
) -> Button<S> {
Button::new(name, ButtonHandlerVariant::Basic(handler))
}

/// Creates a Button whose handler will be given an immutable reference to a user-defined
/// state that is initialized when calling [bind_server]. Almost every struct/function in
/// Btnify has the generic type parameter `S` which represents the user-defined state.
/// You can see an example [here](crate#examples).
///
/// [bind_server]: crate::bind_server
pub fn create_button_with_state(
name: &str,
handler: Box<dyn Send + Sync + Fn(&S) -> ButtonResponse>,
) -> Button<S> {
Button::new(name, ButtonHandlerVariant::WithState(handler))
}

/// Creates a Button that, when clicked, will prompt the user with the `extra_prompts` provided.
/// Those responses will then be given to the Button's handler. Note that the length of the
/// Vec of [ExtraResponse]s given to the handler is guaranteed to be the same length as how
/// many prompts there should be.
pub fn create_button_with_prompts(
name: &str,
handler: Box<dyn Send + Sync + Fn(Vec<ExtraResponse>) -> ButtonResponse>,
extra_prompts: Vec<String>,
) -> Button<S> {
Button::new(
name,
ButtonHandlerVariant::WithExtraPrompts(handler, extra_prompts),
)
}

/// Creates a Button with both [state](Button::create_button_with_state) and [prompts](Button::create_button_with_prompts)
pub fn create_button_with_state_and_prompts(
name: &str,
handler: Box<dyn Send + Sync + Fn(&S, Vec<ExtraResponse>) -> ButtonResponse>,
extra_prompts: Vec<String>,
) -> Button<S> {
Button::new(name, ButtonHandlerVariant::WithBoth(handler, extra_prompts))
}
}

pub(crate) enum ButtonHandlerVariant<S: Send + Sync + 'static> {
Basic(Box<dyn Send + Sync + Fn() -> ButtonResponse>),
WithState(Box<dyn Send + Sync + Fn(&S) -> ButtonResponse>),
WithExtraPrompts(
Box<dyn Send + Sync + Fn(Vec<ExtraResponse>) -> ButtonResponse>,
Vec<String>,
),
WithBoth(
Box<dyn Send + Sync + Fn(&S, Vec<ExtraResponse>) -> ButtonResponse>,
Vec<String>,
),
}

#[derive(Deserialize, Debug)]
pub(crate) struct ButtonInfo {
pub id: usize
// todo: allow any extra data to be sent
pub id: usize,
pub extra_responses: Vec<ExtraResponse>,
}

/// Represents the server's response to a button being pressed. Currently only has a message field.
/// Represents the server's response to a [Button] being pressed. Currently only has a message field.
#[derive(Serialize)]
pub struct ButtonResponse {
pub message: String
pub message: String,
}

impl From<&str> for ButtonResponse {
fn from(message: &str) -> Self {
ButtonResponse { message: message.to_string() }
ButtonResponse {
message: message.to_string(),
}
}
}

Expand All @@ -78,3 +98,9 @@ impl From<String> for ButtonResponse {
ButtonResponse { message }
}
}

/// When a user is asked for an [extra response], there is the option to click "cancel" on the
/// prompt which will result in a `None` variant.
///
/// [extra response]: Button::create_button_with_prompts
pub type ExtraResponse = Option<String>;
Loading

0 comments on commit 5dd51d1

Please sign in to comment.