@@ -1,4 +1,5 @@ | |||
pub mod cardlife; | |||
pub mod robocraft; | |||
#[cfg(test)] | |||
mod tests {} |
@@ -0,0 +1,15 @@ | |||
use crate::robocraft::{DEFAULT_TOKEN}; | |||
pub trait ITokenProvider { | |||
fn token(&self) -> Result<String, ()>; | |||
} | |||
pub struct DefaultTokenProvider { | |||
} | |||
impl ITokenProvider for DefaultTokenProvider { | |||
fn token(&self) -> Result<String, ()> { | |||
Ok(DEFAULT_TOKEN.to_string()) | |||
} | |||
} |
@@ -0,0 +1,59 @@ | |||
use reqwest::{Client, Error}; | |||
use url::{Url}; | |||
use crate::robocraft::{ITokenProvider, DefaultTokenProvider, FactoryInfo, FactorySearchBuilder}; | |||
use crate::robocraft::factory_json::ListPayload; | |||
const FACTORY_DOMAIN: &str = "https://factory.robocraftgame.com/"; | |||
pub struct FactoryAPI { | |||
client: Client, | |||
token: Box<dyn ITokenProvider>, | |||
} | |||
impl FactoryAPI { | |||
pub fn new() -> FactoryAPI { | |||
FactoryAPI { | |||
client: Client::new(), | |||
token: Box::new(DefaultTokenProvider{}), | |||
} | |||
} | |||
pub fn with_auth(token_provider: Box<dyn ITokenProvider>) -> FactoryAPI { | |||
FactoryAPI { | |||
client: Client::new(), | |||
token: token_provider, | |||
} | |||
} | |||
pub async fn list(&self) -> Result<FactoryInfo, Error> { | |||
let url = Url::parse(FACTORY_DOMAIN) | |||
.unwrap() | |||
.join("/api/roboShopItems/list") | |||
.unwrap(); | |||
let payload = ListPayload::default(); | |||
let mut request_builder = self.client.post(url) | |||
.json(&payload); | |||
if let Ok(token) = self.token.token() { | |||
request_builder = request_builder.header("Authorization", "Web ".to_owned() + &token); | |||
} | |||
let result = request_builder.send().await; | |||
if let Ok(response) = result { | |||
return response.json::<FactoryInfo>().await; | |||
} | |||
Err(result.err().unwrap()) | |||
} | |||
pub fn list_builder(&self) -> FactorySearchBuilder { | |||
let url = Url::parse(FACTORY_DOMAIN) | |||
.unwrap() | |||
.join("/api/roboShopItems/list") | |||
.unwrap(); | |||
let mut token_opt = None; | |||
if let Ok(token) = self.token.token() { | |||
token_opt = Some(token); | |||
} | |||
let request_builder = self.client.post(url); | |||
FactorySearchBuilder::new(request_builder, token_opt) | |||
} | |||
} |
@@ -0,0 +1,145 @@ | |||
use serde::{Deserialize, Serialize}; | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub(crate) struct ListPayload { | |||
#[serde(rename = "page")] | |||
pub page: isize, | |||
#[serde(rename = "pageSize")] | |||
pub page_size: isize, | |||
#[serde(rename = "order")] | |||
pub order: isize, | |||
#[serde(rename = "playerFilter")] | |||
pub player_filter: bool, | |||
#[serde(rename = "movementFilter")] | |||
pub movement_filter: String, // csv int enums as str | |||
#[serde(rename = "movementCategoryFilter")] | |||
pub movement_category_filter: String, // csv int enums as str | |||
#[serde(rename = "weaponFilter")] | |||
pub weapon_filter: String, // csv int enums as str | |||
#[serde(rename = "weaponCategoryFilter")] | |||
pub weapon_category_filter: String, // csv int enums as str | |||
#[serde(rename = "minimumCpu")] | |||
pub minimum_cpu: isize, | |||
#[serde(rename = "maximumCpu")] | |||
pub maximum_cpu: isize, | |||
#[serde(rename = "textFilter")] | |||
pub text_filter: String, | |||
#[serde(rename = "textSearchField")] | |||
pub text_search_field: isize, // ??? | |||
#[serde(rename = "buyable")] | |||
pub buyable: bool, | |||
#[serde(rename = "prependFeaturedRobot")] | |||
pub prepend_featured_robot: bool, | |||
#[serde(rename = "featuredOnly")] | |||
pub featured_only: bool, | |||
#[serde(rename = "defaultPage")] | |||
pub default_page: bool, | |||
} | |||
impl ListPayload { | |||
pub fn default() -> ListPayload { | |||
ListPayload { | |||
page: 1, | |||
page_size: 100, | |||
order: 0, | |||
player_filter: false, | |||
movement_filter: "100000,200000,300000,400000,500000,600000,700000,800000,900000,1000000,1100000,1200000".to_string(), | |||
movement_category_filter: "100000,200000,300000,400000,500000,600000,700000,800000,900000,1000000,1100000,1200000".to_string(), | |||
weapon_filter: "10000000,20000000,25000000,30000000,40000000,50000000,60000000,65000000,70100000,75000000".to_string(), | |||
weapon_category_filter: "10000000,20000000,25000000,30000000,40000000,50000000,60000000,65000000,70100000,75000000".to_string(), | |||
minimum_cpu: -1, | |||
maximum_cpu: -1, | |||
text_filter: "".to_string(), | |||
text_search_field: 0, | |||
buyable: true, | |||
prepend_featured_robot: false, | |||
featured_only: false, | |||
default_page: true, | |||
} | |||
} | |||
pub fn empty() -> ListPayload { | |||
ListPayload { | |||
page: 1, | |||
page_size: 100, | |||
order: 0, | |||
player_filter: false, | |||
movement_filter: "".to_string(), | |||
movement_category_filter: "".to_string(), | |||
weapon_filter: "".to_string(), | |||
weapon_category_filter: "".to_string(), | |||
minimum_cpu: -1, | |||
maximum_cpu: -1, | |||
text_filter: "".to_string(), | |||
text_search_field: 0, | |||
buyable: true, | |||
prepend_featured_robot: false, | |||
featured_only: false, | |||
default_page: false, | |||
} | |||
} | |||
} | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct FactoryInfo { | |||
#[serde(rename = "response")] | |||
pub response: RoboShopItemsInfo, | |||
#[serde(rename = "statusCode")] | |||
pub status_code: usize, | |||
} | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct RoboShopItemsInfo { | |||
#[serde(rename = "roboShopItems")] | |||
pub roboshop_items: Vec<FactoryRobotListInfo>, | |||
} | |||
#[derive(Deserialize, Serialize, Clone)] | |||
pub struct FactoryRobotListInfo { | |||
#[serde(rename = "itemId")] | |||
pub item_id: usize, | |||
#[serde(rename = "itemName")] | |||
pub item_name: String, | |||
#[serde(rename = "itemDescription")] | |||
pub item_description: String, | |||
#[serde(rename = "thumbnail")] | |||
pub thumbnail: String, // url | |||
#[serde(rename = "addedBy")] | |||
pub added_by: String, | |||
#[serde(rename = "addedByDisplayName")] | |||
pub added_by_display_name: String, | |||
#[serde(rename = "addedDate")] | |||
pub added_date: String, // ISO date | |||
#[serde(rename = "expiryDate")] | |||
pub expiry_date: String, // ISO date | |||
#[serde(rename = "cpu")] | |||
pub cpu: usize, | |||
#[serde(rename = "totalRobotRanking")] | |||
pub total_robot_ranking: usize, | |||
#[serde(rename = "rentCount")] | |||
pub rent_count: usize, | |||
#[serde(rename = "buyCount")] | |||
pub buy_count: usize, | |||
#[serde(rename = "buyable")] | |||
pub buyable: bool, | |||
#[serde(rename = "removedDate")] | |||
pub removed_date: Option<String>, | |||
#[serde(rename = "banDate")] | |||
pub ban_date: Option<String>, | |||
#[serde(rename = "featured")] | |||
pub featured: bool, | |||
#[serde(rename = "bannerMessage")] | |||
pub banner_message: Option<String>, | |||
#[serde(rename = "combatRating")] | |||
pub combat_rating: f32, | |||
#[serde(rename = "cosmeticRating")] | |||
pub cosmetic_rating: f32, | |||
#[serde(rename = "cubeAmounts")] | |||
pub cube_amounts: String, // JSON as str | |||
} | |||
impl std::string::ToString for FactoryRobotListInfo { | |||
fn to_string(&self) -> String { | |||
format!("{} by {} ({})", &self.item_name, &self.added_by_display_name, &self.item_id) | |||
} | |||
} |
@@ -0,0 +1,170 @@ | |||
use reqwest::{RequestBuilder, Error}; | |||
use crate::robocraft::{FactoryInfo}; | |||
use crate::robocraft::factory_json::ListPayload; | |||
pub enum FactoryOrderType { | |||
Suggested = 0, | |||
CombatRating = 1, | |||
CosmeticRating = 2, | |||
Added = 3, | |||
CPU = 4, | |||
MostBought = 5, | |||
} | |||
pub enum FactoryMovementType { | |||
Wheels = 100000, | |||
Hovers = 200000, | |||
Aerofoils=300000, | |||
Thrusters=400000, | |||
Rudders=500000, | |||
InsectLegs=600000, | |||
MechLegs=700000, | |||
Skis=800000, | |||
TankTreads=900000, | |||
Rotors=1000000, | |||
Sprinters=1100000, | |||
Propellers=1200000 | |||
} | |||
pub enum FactoryWeaponType { | |||
Laser=10000000, | |||
PlasmaLauncher=20000000, | |||
GyroMortar=25000000, | |||
RailCannon=30000000, | |||
NanoDisruptor=40000000, | |||
TeslaBlade=50000000, | |||
AeroflakCannon=60000000, | |||
IonCannon=65000000, | |||
ProtoSeeker=70100000, | |||
ChainShredder=75000000, | |||
} | |||
pub enum FactoryTextSearchType { | |||
All=0, | |||
Player=1, | |||
Name=2, | |||
} | |||
pub struct FactorySearchBuilder { | |||
reqwest_builder: RequestBuilder, | |||
payload: ListPayload, | |||
token: Option<String>, | |||
} | |||
impl FactorySearchBuilder { | |||
pub(crate) fn new(request_builder: RequestBuilder, token: Option<String>) -> FactorySearchBuilder { | |||
FactorySearchBuilder { | |||
reqwest_builder: request_builder, | |||
payload: ListPayload::empty(), | |||
token, | |||
} | |||
} | |||
pub fn page(mut self, page_number: isize) -> Self { | |||
self.payload.page = page_number; | |||
self | |||
} | |||
pub fn items_per_page(mut self, page_size: isize) -> Self { | |||
self.payload.page_size = page_size; | |||
self | |||
} | |||
pub fn order(mut self, order_type: FactoryOrderType) -> Self { | |||
self.payload.order = order_type as isize; | |||
self | |||
} | |||
/* // this appears to not do anything (removed to prevent confusion) | |||
// use text_search_type(FactoryTextSearchType::Player) instead | |||
pub fn players_only(mut self, p: bool) -> Self { | |||
self.payload.player_filter = p; | |||
self | |||
} | |||
*/ | |||
pub fn movement_or(mut self, movement_type: FactoryMovementType) -> Self { | |||
if self.payload.movement_filter == "" { | |||
self.payload.movement_filter = format!("{},{}", &self.payload.movement_filter, movement_type as isize); | |||
} else { | |||
self.payload.movement_filter = (movement_type as isize).to_string(); | |||
} | |||
self.payload.movement_category_filter = self.payload.movement_filter.clone(); | |||
self | |||
} | |||
pub fn weapon_or(mut self, weapon_type: FactoryWeaponType) -> Self { | |||
if self.payload.weapon_filter == "" { | |||
self.payload.weapon_filter = format!("{},{}", &self.payload.weapon_filter, weapon_type as isize); | |||
} else { | |||
self.payload.weapon_filter = (weapon_type as isize).to_string(); | |||
} | |||
self.payload.weapon_category_filter = self.payload.weapon_filter.clone(); | |||
self | |||
} | |||
pub fn cpu_range(mut self, min: isize, max: isize) -> Self { | |||
self.payload.minimum_cpu = min; | |||
self.payload.maximum_cpu = max; | |||
self | |||
} | |||
pub fn min_cpu(mut self, min: isize) -> Self { | |||
self.payload.minimum_cpu = min; | |||
self | |||
} | |||
pub fn max_cpu(mut self, max: isize) -> Self { | |||
self.payload.maximum_cpu = max; | |||
self | |||
} | |||
pub fn no_minimum_cpu(mut self) -> Self { | |||
self.payload.minimum_cpu = -1; | |||
self | |||
} | |||
pub fn no_maximum_cpu(mut self) -> Self { | |||
self.payload.maximum_cpu = -1; | |||
self | |||
} | |||
pub fn text(mut self, t: String) -> Self { | |||
self.payload.text_filter = t; | |||
self | |||
} | |||
pub fn text_search_type(mut self, search_type: FactoryTextSearchType) -> Self { | |||
self.payload.text_search_field = search_type as isize; | |||
self | |||
} | |||
// setting buyable to false while using the default token provider will cause HTTP status 500 error | |||
pub fn buyable(mut self, b: bool) -> Self { | |||
self.payload.buyable = b; | |||
self | |||
} | |||
pub fn prepend_featured(mut self, b: bool) -> Self { | |||
self.payload.prepend_featured_robot = b; | |||
self | |||
} | |||
pub fn default_page(mut self, b: bool) -> Self { | |||
self.payload.default_page = b; | |||
self | |||
} | |||
pub async fn send(mut self) -> Result<FactoryInfo, Error> { | |||
self.reqwest_builder = self.reqwest_builder.json(&self.payload); | |||
if let Some(token) = self.token.clone() { | |||
self.reqwest_builder = self.reqwest_builder.header("Authorization", "Web ".to_owned() + &token); | |||
} | |||
let result = self.reqwest_builder.send().await; | |||
if let Ok(response) = result { | |||
return response.json::<FactoryInfo>().await; | |||
} | |||
Err(result.err().unwrap()) | |||
} | |||
} |
@@ -0,0 +1,11 @@ | |||
mod factory; | |||
mod factory_json; | |||
mod factory_request_builder; | |||
pub use self::factory::{FactoryAPI}; | |||
pub use self::factory_json::{FactoryInfo, FactoryRobotListInfo, RoboShopItemsInfo}; | |||
pub use self::factory_request_builder::{FactorySearchBuilder, FactoryMovementType, FactoryOrderType, FactoryWeaponType, FactoryTextSearchType}; | |||
mod auth; | |||
pub use self::auth::{ITokenProvider, DefaultTokenProvider}; | |||
pub const DEFAULT_TOKEN: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJQdWJsaWNJZCI6IjEyMyIsIkRpc3BsYXlOYW1lIjoiVGVzdCIsIlJvYm9jcmFmdE5hbWUiOiJGYWtlQ1JGVXNlciIsIkZsYWdzIjpbXSwiaXNzIjoiRnJlZWphbSIsInN1YiI6IldlYiIsImlhdCI6MTU0NTIyMzczMiwiZXhwIjoyNTQ1MjIzNzkyfQ.ralLmxdMK9rVKPZxGng8luRIdbTflJ4YMJcd25dKlqg"; |
@@ -6,7 +6,7 @@ fn clre_server_init() -> Result<(), ()> { | |||
Ok(()) | |||
} | |||
#[tokio::test] | |||
/*#[tokio::test] | |||
async fn clre_server_game() -> Result<(), ()> { | |||
let server = cardlife::CLreServer::new("http://localhost:5030").unwrap(); | |||
let result = server.game_info().await; | |||
@@ -36,4 +36,4 @@ async fn clre_server_status() -> Result<(), ()> { | |||
} | |||
} | |||
Ok(()) | |||
} | |||
}*/ |
@@ -0,0 +1,78 @@ | |||
use libfj::robocraft; | |||
#[test] | |||
fn robocraft_factory_api_init() -> Result<(), ()> { | |||
robocraft::FactoryAPI::new(); | |||
Ok(()) | |||
} | |||
#[tokio::test] | |||
async fn robocraft_factory_default_query() -> Result<(), ()> { | |||
let api = robocraft::FactoryAPI::new(); | |||
let result = api.list().await; | |||
assert!(result.is_ok()); | |||
let robo_info = result.unwrap(); | |||
assert_ne!(robo_info.response.roboshop_items.len(), 0); | |||
assert_eq!(robo_info.status_code, 200); | |||
for robot in &robo_info.response.roboshop_items { | |||
assert_ne!(robot.item_name, ""); | |||
assert_ne!(robot.added_by, ""); | |||
assert_ne!(robot.added_by_display_name, ""); | |||
assert_ne!(robot.thumbnail, ""); | |||
println!("FactoryRobotListInfo.to_string() -> `{}`", robot.to_string()); | |||
} | |||
Ok(()) | |||
} | |||
fn builder() -> robocraft::FactorySearchBuilder { | |||
robocraft::FactoryAPI::new().list_builder() | |||
} | |||
fn assert_factory_list(robo_info: robocraft::FactoryInfo) -> Result<(), ()> { | |||
assert_ne!(robo_info.response.roboshop_items.len(), 0); | |||
assert_eq!(robo_info.status_code, 200); | |||
for robot in &robo_info.response.roboshop_items { | |||
assert_ne!(robot.item_name, ""); | |||
assert_ne!(robot.added_by, ""); | |||
assert_ne!(robot.added_by_display_name, ""); | |||
assert_ne!(robot.thumbnail, ""); | |||
println!("FactoryRobotListInfo.to_string() -> `{}`", robot.to_string()); | |||
} | |||
Ok(()) | |||
} | |||
#[tokio::test] | |||
async fn robocraft_factory_custom_query() -> Result<(), ()> { | |||
let api = robocraft::FactoryAPI::new(); | |||
let result = api.list_builder() | |||
.movement_or(robocraft::FactoryMovementType::Wheels) | |||
.weapon_or(robocraft::FactoryWeaponType::Laser) | |||
.page(2) | |||
.items_per_page(10) | |||
.send().await; | |||
assert!(result.is_ok()); | |||
let robo_info = result.unwrap(); | |||
assert_ne!(robo_info.response.roboshop_items.len(), 0); | |||
//assert_eq!(robo_info.response.roboshop_items.len(), 16); the API behaviour is weird, I swear it's not me! | |||
assert!(robo_info.response.roboshop_items.len() >= 10); | |||
assert_eq!(robo_info.status_code, 200); | |||
for robot in &robo_info.response.roboshop_items { | |||
assert_ne!(robot.item_name, ""); | |||
assert_ne!(robot.added_by, ""); | |||
assert_ne!(robot.added_by_display_name, ""); | |||
assert_ne!(robot.thumbnail, ""); | |||
println!("FactoryRobotListInfo.to_string() -> `{}`", robot.to_string()); | |||
} | |||
Ok(()) | |||
} | |||
#[tokio::test] | |||
async fn robocraft_factory_player_query() -> Result<(), ()> { | |||
let result = builder() | |||
.text("Baerentoeter".to_string()) | |||
.text_search_type(robocraft::FactoryTextSearchType::Player) | |||
.items_per_page(10) | |||
.send().await; | |||
assert!(result.is_ok()); | |||
assert_factory_list(result.unwrap()) | |||
} |