diff --git a/src/functions/builtins.rs b/src/functions/builtins.rs index 1f09132..3184bec 100644 --- a/src/functions/builtins.rs +++ b/src/functions/builtins.rs @@ -1,11 +1,11 @@ -pub mod math; -pub mod api; -pub mod system; -pub mod dev; -pub mod network; -pub mod array; -pub mod str; -pub mod trig; -pub mod crypto; - -pub use super::*; \ No newline at end of file +pub mod api; +pub mod array; +pub mod crypto; +pub mod dev; +pub mod math; +pub mod network; +pub mod str; +pub mod system; +pub mod trig; + +pub use super::*; diff --git a/src/functions/builtins/str.rs b/src/functions/builtins/str.rs index 719e421..8d707ba 100644 --- a/src/functions/builtins/str.rs +++ b/src/functions/builtins/str.rs @@ -1,310 +1,499 @@ -//! Builtin functions for string manipulation - -use regex::Regex; -use super::*; -use crate::value::{Value, IntegerType}; -use crate::ExpectedTypes; - -const CONTAINS : FunctionDefinition = FunctionDefinition { - name: "contains", - category: Some("strings"), - description: "Returns true if array or string [source] contains [s]", - arguments: || vec![ - FunctionArgument::new_required("source", ExpectedTypes::Any), - FunctionArgument::new_required("s", ExpectedTypes::Any) - ], - handler: |_function, _token, _state, args| { - let source = args.get("source").required(); - let s = args.get("s").required(); - match source.is_array() { - true => Ok(Value::Boolean(source.as_array().contains(&s))), - false => Ok(Value::Boolean(source.to_string().contains(&s.to_string()))) - } - } -}; - -const CONCAT : FunctionDefinition = FunctionDefinition { - name: "concat", - category: Some("strings"), - description: "Concatenate a set of strings", - arguments: || vec![ - FunctionArgument::new_plural("s", ExpectedTypes::String, true) - ], - handler: |_function, _token, _state, args| { - Ok(Value::String(args.iter().map(|v|v.as_string()).collect::())) - } -}; - -const STRLEN : FunctionDefinition = FunctionDefinition { - name: "strlen", - category: Some("strings"), - description: "Returns the length of the string s", - arguments: || vec![ - FunctionArgument::new_required("s", ExpectedTypes::String) - ], - handler: |_function, _token, _state, args| { - let s = args.get("s").required().as_string(); - Ok(Value::Integer(s.len() as IntegerType)) - } -}; - -const UPPERCASE : FunctionDefinition = FunctionDefinition { - name: "uppercase", - category: Some("strings"), - description: "Converts the string s to uppercase", - arguments: || vec![ - FunctionArgument::new_required("s", ExpectedTypes::String) - ], - handler: |_function, _token, _state, args| { - let s = args.get("s").required().as_string(); - Ok(Value::String(s.to_uppercase())) - } -}; - -const LOWERCASE : FunctionDefinition = FunctionDefinition { - name: "lowercase", - category: Some("strings"), - description: "Converts the string s to lowercase", - arguments: || vec![ - FunctionArgument::new_required("s", ExpectedTypes::String) - ], - handler: |_function, _token, _state, args| { - let s = args.get("s").required().as_string(); - Ok(Value::String(s.to_lowercase())) - } -}; - -const TRIM : FunctionDefinition = FunctionDefinition { - name: "trim", - category: Some("strings"), - description: "Trim whitespace from a string", - arguments: || vec![ - FunctionArgument::new_required("s", ExpectedTypes::String) - ], - handler: |_function, _token, _state, args| { - let s = args.get("s").required().as_string(); - Ok(Value::String(s.trim().to_string())) - } -}; - -const SUBSTR : FunctionDefinition = FunctionDefinition { - name: "substr", - category: Some("strings"), - description: "Returns a substring from s, beginning at [start], and going to the end, or for [length] characters", - arguments: || vec![ - FunctionArgument::new_required("s", ExpectedTypes::String), - FunctionArgument::new_required("start", ExpectedTypes::IntOrFloat), - FunctionArgument::new_optional("length", ExpectedTypes::IntOrFloat) - ], - handler: |function, token, _state, args| { - let s = args.get("s").required().as_string(); - let start = args.get("start").required().as_int().unwrap_or(0); - let default_len = s.len() as IntegerType - start; - let length = match args.get("length").optional() { - Some(l) => l, - None => Value::Integer(default_len) - }.as_int().unwrap_or(default_len); - - if start >= s.len() as IntegerType || start < 0 { - return Err(Error::FunctionArgumentOverflow { - arg: 2, - signature: function.signature(), - token: token.clone() - }); - } else if length < 0 || length > (s.len() - start as usize) as IntegerType { - return Err(Error::FunctionArgumentOverflow { - arg: 3, - signature: function.signature(), - token: token.clone() - }); - } - - Ok(Value::String(s.chars().skip(start as usize).take(length as usize).collect())) - } -}; - -const REGEX : FunctionDefinition = FunctionDefinition { - name: "regex", - category: Some("strings"), - description: "Returns a regular expression match from [subject], or false", - arguments: || vec![ - FunctionArgument::new_required("pattern", ExpectedTypes::String), - FunctionArgument::new_required("subject", ExpectedTypes::String), - FunctionArgument::new_optional("group", ExpectedTypes::Int) - ], - handler: |_function, token, _state, args| { - let pattern = args.get("pattern").required().as_string(); - let subject = args.get("subject").required().as_string(); - let group = match args.get("group").optional() { - Some(g) => g.as_int(), - None => None - }; - - let re = Regex::new(&pattern); - if let Err(_) = re { - return Err(Error::StringFormat { expected_format: "regex".to_string(), token: token.clone() }); - } - - if let Some(caps) = re.unwrap().captures(&subject) { - match group { - Some(g) => { - let group_index = g; - if let Some(group) = caps.get(group_index as usize) { - return Ok(Value::String(group.as_str().to_string())); - } - }, - None => { - return Ok(Value::String(caps.get(0).unwrap().as_str().to_string())); - } - } - } - - Ok(Value::Boolean(false)) - } -}; - -/// Register string functions -pub fn register_functions(table: &mut FunctionTable) { - table.register(CONTAINS); - table.register(CONCAT); - table.register(STRLEN); - table.register(UPPERCASE); - table.register(LOWERCASE); - table.register(TRIM); - table.register(SUBSTR); - table.register(REGEX); -} - -#[cfg(test)] -mod test_builtin_functions { - use super::*; - - #[test] - fn test_regex() { - let mut state = ParserState::new(); - - assert_eq!(Value::Boolean(false), REGEX.call(&Token::dummy(""), &mut state, &[ - Value::String("test".to_string()), Value::String("bar".to_string()) - ]).unwrap()); - assert_eq!(Value::String("foo".to_string()), REGEX.call(&Token::dummy(""), &mut state, &[ - Value::String("foo".to_string()), Value::String("foobar".to_string()) - ]).unwrap()); - assert_eq!(Value::String("foobar".to_string()), REGEX.call(&Token::dummy(""), &mut state, &[ - Value::String("foo.*".to_string()), Value::String("foobar".to_string()) - ]).unwrap()); - assert_eq!(Value::String("bar".to_string()), REGEX.call(&Token::dummy(""), &mut state, &[ - Value::String("foo(.*)".to_string()), Value::String("foobar".to_string()), - Value::Integer(1) - ]).unwrap()); - assert_eq!(Value::String("foobar".to_string()), REGEX.call(&Token::dummy(""), &mut state, &[ - Value::String("foo(.*)".to_string()), Value::String("foobar".to_string()), - Value::Integer(0) - ]).unwrap()); - assert_eq!(Value::Boolean(false), REGEX.call(&Token::dummy(""), &mut state, &[ - Value::String("foo(.*)".to_string()), Value::String("foobar".to_string()), - Value::Integer(6) - ]).unwrap()); - } - - #[test] - fn test_strlen() { - let mut state = ParserState::new(); - - assert_eq!(Value::Integer(0), STRLEN.call(&Token::dummy(""), &mut state, - &[Value::String("".to_string())]).unwrap()); - assert_eq!(Value::Integer(3), STRLEN.call(&Token::dummy(""), &mut state, - &[Value::String(" ".to_string())]).unwrap()); - } - - #[test] - fn test_uppercase() { - let mut state = ParserState::new(); - - assert_eq!(Value::String("TEST".to_string()), UPPERCASE.call(&Token::dummy(""), &mut state, - &[Value::String("test".to_string())]).unwrap()); - assert_eq!(Value::String(" TEST ".to_string()), UPPERCASE.call(&Token::dummy(""), &mut state, - &[Value::String(" test ".to_string())]).unwrap()); - } - - - #[test] - fn test_lowercase() { - let mut state = ParserState::new(); - - assert_eq!(Value::String("test".to_string()), LOWERCASE.call(&Token::dummy(""), &mut state, - &[Value::String("TEST".to_string())]).unwrap()); - assert_eq!(Value::String(" test ".to_string()), LOWERCASE.call(&Token::dummy(""), &mut state, - &[Value::String(" TEST ".to_string())]).unwrap()); - } - - - #[test] - fn test_trim() { - let mut state = ParserState::new(); - - assert_eq!(Value::String("test".to_string()), TRIM.call(&Token::dummy(""), &mut state, - &[Value::String("test".to_string())]).unwrap()); - assert_eq!(Value::String("TEST".to_string()), TRIM.call(&Token::dummy(""), &mut state, - &[Value::String(" TEST ".to_string())]).unwrap()); - } - - #[test] - fn test_concat() { - let mut state = ParserState::new(); - - assert_eq!(Value::String(" ".to_string()), CONCAT.call( - &Token::dummy(""), - &mut state, &[Value::String("".to_string()), - Value::String(" ".to_string()) - ]).unwrap()); - assert_eq!(Value::String("test4false".to_string()), CONCAT.call( - &Token::dummy(""), - &mut state, &[Value::String("test".to_string()), - Value::Integer(4), - Value::Boolean(false) - ]).unwrap()); - } - - #[test] - fn test_substr() { - let mut state = ParserState::new(); - - assert_eq!(Value::String("t".to_string()), - SUBSTR.call(&Token::dummy(""), &mut state, &[Value::String("test".to_string()), Value::Integer(3)]).unwrap() - ); - assert_eq!(Value::String("tes".to_string()), - SUBSTR.call(&Token::dummy(""), &mut state, &[Value::String("test".to_string()), Value::Integer(0), Value::Integer(3)]).unwrap() - ); - } - - #[test] - fn test_contains() { - let mut state = ParserState::new(); - - assert_eq!(Value::Boolean(true), CONTAINS.call(&Token::dummy(""), &mut state, &[ - Value::String("test".to_string()), - Value::String("e".to_string()) - ]).unwrap()); - - assert_eq!(Value::Boolean(false), CONTAINS.call(&Token::dummy(""), &mut state, &[ - Value::String("test".to_string()), - Value::String("fff".to_string()) - ]).unwrap()); - - assert_eq!(Value::Boolean(true), CONTAINS.call(&Token::dummy(""), &mut state, &[ - Value::Array(vec![ - Value::Integer(5), - Value::Integer(3), - ]), - Value::Integer(5) - ]).unwrap()); - - assert_eq!(Value::Boolean(false), CONTAINS.call(&Token::dummy(""), &mut state, &[ - Value::Array(vec![ - Value::Integer(5), - Value::Integer(3), - ]), - Value::Integer(4) - ]).unwrap()); - } -} \ No newline at end of file +//! Builtin functions for string manipulation + +use super::*; +use crate::value::{IntegerType, Value}; +use crate::ExpectedTypes; +use regex::Regex; + +const CONTAINS: FunctionDefinition = FunctionDefinition { + name: "contains", + category: Some("strings"), + description: "Returns true if array or string [source] contains [s]", + arguments: || { + vec![ + FunctionArgument::new_required("source", ExpectedTypes::Any), + FunctionArgument::new_required("s", ExpectedTypes::Any), + ] + }, + handler: |_function, _token, _state, args| { + let source = args.get("source").required(); + let s = args.get("s").required(); + match source.is_array() { + true => Ok(Value::Boolean(source.as_array().contains(&s))), + false => Ok(Value::Boolean(source.to_string().contains(&s.to_string()))), + } + }, +}; + +const CONCAT: FunctionDefinition = FunctionDefinition { + name: "concat", + category: Some("strings"), + description: "Concatenate a set of strings", + arguments: || { + vec![FunctionArgument::new_plural( + "s", + ExpectedTypes::String, + true, + )] + }, + handler: |_function, _token, _state, args| { + Ok(Value::String( + args.iter().map(|v| v.as_string()).collect::(), + )) + }, +}; + +const STRLEN: FunctionDefinition = FunctionDefinition { + name: "strlen", + category: Some("strings"), + description: "Returns the length of the string s", + arguments: || vec![FunctionArgument::new_required("s", ExpectedTypes::String)], + handler: |_function, _token, _state, args| { + let s = args.get("s").required().as_string(); + Ok(Value::Integer(s.len() as IntegerType)) + }, +}; + +const UPPERCASE: FunctionDefinition = FunctionDefinition { + name: "uppercase", + category: Some("strings"), + description: "Converts the string s to uppercase", + arguments: || vec![FunctionArgument::new_required("s", ExpectedTypes::String)], + handler: |_function, _token, _state, args| { + let s = args.get("s").required().as_string(); + Ok(Value::String(s.to_uppercase())) + }, +}; + +const LOWERCASE: FunctionDefinition = FunctionDefinition { + name: "lowercase", + category: Some("strings"), + description: "Converts the string s to lowercase", + arguments: || vec![FunctionArgument::new_required("s", ExpectedTypes::String)], + handler: |_function, _token, _state, args| { + let s = args.get("s").required().as_string(); + Ok(Value::String(s.to_lowercase())) + }, +}; + +const TRIM: FunctionDefinition = FunctionDefinition { + name: "trim", + category: Some("strings"), + description: "Trim whitespace from a string", + arguments: || vec![FunctionArgument::new_required("s", ExpectedTypes::String)], + handler: |_function, _token, _state, args| { + let s = args.get("s").required().as_string(); + Ok(Value::String(s.trim().to_string())) + }, +}; + +const SUBSTR : FunctionDefinition = FunctionDefinition { + name: "substr", + category: Some("strings"), + description: "Returns a substring from s, beginning at [start], and going to the end, or for [length] characters", + arguments: || vec![ + FunctionArgument::new_required("s", ExpectedTypes::String), + FunctionArgument::new_required("start", ExpectedTypes::IntOrFloat), + FunctionArgument::new_optional("length", ExpectedTypes::IntOrFloat) + ], + handler: |function, token, _state, args| { + let s = args.get("s").required().as_string(); + let start = args.get("start").required().as_int().unwrap_or(0); + let default_len = s.len() as IntegerType - start; + let length = match args.get("length").optional() { + Some(l) => l, + None => Value::Integer(default_len) + }.as_int().unwrap_or(default_len); + + if start >= s.len() as IntegerType || start < 0 { + return Err(Error::FunctionArgumentOverflow { + arg: 2, + signature: function.signature(), + token: token.clone() + }); + } else if length < 0 || length > (s.len() - start as usize) as IntegerType { + return Err(Error::FunctionArgumentOverflow { + arg: 3, + signature: function.signature(), + token: token.clone() + }); + } + + Ok(Value::String(s.chars().skip(start as usize).take(length as usize).collect())) + } +}; + +const REGEX: FunctionDefinition = FunctionDefinition { + name: "regex", + category: Some("strings"), + description: "Returns a regular expression match from [subject], or false", + arguments: || { + vec![ + FunctionArgument::new_required("pattern", ExpectedTypes::String), + FunctionArgument::new_required("subject", ExpectedTypes::String), + FunctionArgument::new_optional("group", ExpectedTypes::Int), + ] + }, + handler: |_function, token, _state, args| { + let pattern = args.get("pattern").required().as_string(); + let subject = args.get("subject").required().as_string(); + let group = match args.get("group").optional() { + Some(g) => g.as_int(), + None => None, + }; + + let re = Regex::new(&pattern); + if let Err(_) = re { + return Err(Error::StringFormat { + expected_format: "regex".to_string(), + token: token.clone(), + }); + } + + if let Some(caps) = re.unwrap().captures(&subject) { + match group { + Some(g) => { + let group_index = g; + if let Some(group) = caps.get(group_index as usize) { + return Ok(Value::String(group.as_str().to_string())); + } + } + None => { + return Ok(Value::String(caps.get(0).unwrap().as_str().to_string())); + } + } + } + + Ok(Value::Boolean(false)) + }, +}; + +/// Register string functions +pub fn register_functions(table: &mut FunctionTable) { + table.register(CONTAINS); + table.register(CONCAT); + table.register(STRLEN); + table.register(UPPERCASE); + table.register(LOWERCASE); + table.register(TRIM); + table.register(SUBSTR); + table.register(REGEX); +} + +#[cfg(test)] +mod test_builtin_functions { + use super::*; + + #[test] + fn test_regex() { + let mut state = ParserState::new(); + + assert_eq!( + Value::Boolean(false), + REGEX + .call( + &Token::dummy(""), + &mut state, + &[ + Value::String("test".to_string()), + Value::String("bar".to_string()) + ] + ) + .unwrap() + ); + assert_eq!( + Value::String("foo".to_string()), + REGEX + .call( + &Token::dummy(""), + &mut state, + &[ + Value::String("foo".to_string()), + Value::String("foobar".to_string()) + ] + ) + .unwrap() + ); + assert_eq!( + Value::String("foobar".to_string()), + REGEX + .call( + &Token::dummy(""), + &mut state, + &[ + Value::String("foo.*".to_string()), + Value::String("foobar".to_string()) + ] + ) + .unwrap() + ); + assert_eq!( + Value::String("bar".to_string()), + REGEX + .call( + &Token::dummy(""), + &mut state, + &[ + Value::String("foo(.*)".to_string()), + Value::String("foobar".to_string()), + Value::Integer(1) + ] + ) + .unwrap() + ); + assert_eq!( + Value::String("foobar".to_string()), + REGEX + .call( + &Token::dummy(""), + &mut state, + &[ + Value::String("foo(.*)".to_string()), + Value::String("foobar".to_string()), + Value::Integer(0) + ] + ) + .unwrap() + ); + assert_eq!( + Value::Boolean(false), + REGEX + .call( + &Token::dummy(""), + &mut state, + &[ + Value::String("foo(.*)".to_string()), + Value::String("foobar".to_string()), + Value::Integer(6) + ] + ) + .unwrap() + ); + } + + #[test] + fn test_strlen() { + let mut state = ParserState::new(); + + assert_eq!( + Value::Integer(0), + STRLEN + .call( + &Token::dummy(""), + &mut state, + &[Value::String("".to_string())] + ) + .unwrap() + ); + assert_eq!( + Value::Integer(3), + STRLEN + .call( + &Token::dummy(""), + &mut state, + &[Value::String(" ".to_string())] + ) + .unwrap() + ); + } + + #[test] + fn test_uppercase() { + let mut state = ParserState::new(); + + assert_eq!( + Value::String("TEST".to_string()), + UPPERCASE + .call( + &Token::dummy(""), + &mut state, + &[Value::String("test".to_string())] + ) + .unwrap() + ); + assert_eq!( + Value::String(" TEST ".to_string()), + UPPERCASE + .call( + &Token::dummy(""), + &mut state, + &[Value::String(" test ".to_string())] + ) + .unwrap() + ); + } + + #[test] + fn test_lowercase() { + let mut state = ParserState::new(); + + assert_eq!( + Value::String("test".to_string()), + LOWERCASE + .call( + &Token::dummy(""), + &mut state, + &[Value::String("TEST".to_string())] + ) + .unwrap() + ); + assert_eq!( + Value::String(" test ".to_string()), + LOWERCASE + .call( + &Token::dummy(""), + &mut state, + &[Value::String(" TEST ".to_string())] + ) + .unwrap() + ); + } + + #[test] + fn test_trim() { + let mut state = ParserState::new(); + + assert_eq!( + Value::String("test".to_string()), + TRIM.call( + &Token::dummy(""), + &mut state, + &[Value::String("test".to_string())] + ) + .unwrap() + ); + assert_eq!( + Value::String("TEST".to_string()), + TRIM.call( + &Token::dummy(""), + &mut state, + &[Value::String(" TEST ".to_string())] + ) + .unwrap() + ); + } + + #[test] + fn test_concat() { + let mut state = ParserState::new(); + + assert_eq!( + Value::String(" ".to_string()), + CONCAT + .call( + &Token::dummy(""), + &mut state, + &[ + Value::String("".to_string()), + Value::String(" ".to_string()) + ] + ) + .unwrap() + ); + assert_eq!( + Value::String("test4false".to_string()), + CONCAT + .call( + &Token::dummy(""), + &mut state, + &[ + Value::String("test".to_string()), + Value::Integer(4), + Value::Boolean(false) + ] + ) + .unwrap() + ); + } + + #[test] + fn test_substr() { + let mut state = ParserState::new(); + + assert_eq!( + Value::String("t".to_string()), + SUBSTR + .call( + &Token::dummy(""), + &mut state, + &[Value::String("test".to_string()), Value::Integer(3)] + ) + .unwrap() + ); + assert_eq!( + Value::String("tes".to_string()), + SUBSTR + .call( + &Token::dummy(""), + &mut state, + &[ + Value::String("test".to_string()), + Value::Integer(0), + Value::Integer(3) + ] + ) + .unwrap() + ); + } + + #[test] + fn test_contains() { + let mut state = ParserState::new(); + + assert_eq!( + Value::Boolean(true), + CONTAINS + .call( + &Token::dummy(""), + &mut state, + &[ + Value::String("test".to_string()), + Value::String("e".to_string()) + ] + ) + .unwrap() + ); + + assert_eq!( + Value::Boolean(false), + CONTAINS + .call( + &Token::dummy(""), + &mut state, + &[ + Value::String("test".to_string()), + Value::String("fff".to_string()) + ] + ) + .unwrap() + ); + + assert_eq!( + Value::Boolean(true), + CONTAINS + .call( + &Token::dummy(""), + &mut state, + &[ + Value::Array(vec![Value::Integer(5), Value::Integer(3),]), + Value::Integer(5) + ] + ) + .unwrap() + ); + + assert_eq!( + Value::Boolean(false), + CONTAINS + .call( + &Token::dummy(""), + &mut state, + &[ + Value::Array(vec![Value::Integer(5), Value::Integer(3),]), + Value::Integer(4) + ] + ) + .unwrap() + ); + } +} diff --git a/src/network/api_instance.rs b/src/network/api_instance.rs index e698635..4f2194e 100644 --- a/src/network/api_instance.rs +++ b/src/network/api_instance.rs @@ -1,116 +1,142 @@ -use crate::value::Value; -use crate::network::utils::*; - -use std::collections::HashMap; -use std::fmt; - -/// Represents an instance of an API -#[derive(Clone)] -pub struct ApiInstance { - base_url: String, - description: String, - examples: String, - key: Option, -} - -impl ApiInstance { - /// Create a new API instance - /// - /// # Arguments - /// * `base_url` - base url for the API - pub fn new(base_url: String) -> Self { - Self { base_url: base_url.trim_end_matches('/').to_string(), description: "".to_string(), examples: "".to_string(), key: None } - } - - /// Create a new API instance with an API key - /// - /// # Arguments - /// * `base_url` - base url for the API - /// * `key` - API key - pub fn new_with_key(base_url: String, key: String) -> Self { - let mut i = Self::new(base_url); - i.set_key(key); - i - } - - /// Create a new API instance with a description - /// - /// # Arguments - /// * `base_url` - base url for the API - /// * `description` - API description - /// * `description` - API examples - pub fn new_with_description(base_url: String, description: String, examples: String) -> Self { - let mut i = Self::new(base_url); - i.set_description(description); - i.set_examples(examples); - i - } - - /// Return the base url - pub fn base_url(&self) -> &String { - &self.base_url - } - - /// Set the API key credential for the API - /// - /// # Arguments - /// * `key` - API key - pub fn set_key(&mut self, key: String) -> &Self { - self.key = Some(key); - self - } - - /// Return the examples - pub fn examples(&self) -> &String { - &self.examples - } - - /// Set the examples for the API - /// - /// # Arguments - /// * `examples` - API examples - pub fn set_examples(&mut self, examples: String) -> &Self { - self.examples = examples; - self - } - - /// Return the description - pub fn description(&self) -> &String { - &self.description - } - - /// Set the description for the API - /// - /// # Arguments - /// * `description` - API description - pub fn set_description(&mut self, description: String) -> &Self { - self.description = description; - self - } - - /// Return the API key - pub fn key(&self) -> &Option { - &self.key - } - - /// Make a request to the API - /// - /// # Arguments - /// * `endpoint` - Endpoint to call - /// * `body` - Supply a body for POST, or None for GET - /// * `headers` - Vec of extra headers to supply to the API - pub fn request(&self, endpoint: &str, body: Option, headers: HashMap) -> Result { - let url = format!("{}/{}", self.base_url(), endpoint); - request(&url, body, headers) - } -} - -impl fmt::Display for ApiInstance { - // This trait requires `fmt` with this exact signature. - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let description = format!("{}{}", if self.description().is_empty() {""} else {"\n\t"}, self.description()); - let examples = format!("{}{}", if self.examples().is_empty() {""} else {"\n\t"}, self.examples()); - - write!(f, "{}{}{}", self.base_url(), description, examples) - } -} \ No newline at end of file +use crate::network::utils::*; +use crate::value::Value; + +use std::collections::HashMap; +use std::fmt; + +/// Represents an instance of an API +#[derive(Clone)] +pub struct ApiInstance { + base_url: String, + description: String, + examples: String, + key: Option, +} + +impl ApiInstance { + /// Create a new API instance + /// + /// # Arguments + /// * `base_url` - base url for the API + pub fn new(base_url: String) -> Self { + Self { + base_url: base_url.trim_end_matches('/').to_string(), + description: "".to_string(), + examples: "".to_string(), + key: None, + } + } + + /// Create a new API instance with an API key + /// + /// # Arguments + /// * `base_url` - base url for the API + /// * `key` - API key + pub fn new_with_key(base_url: String, key: String) -> Self { + let mut i = Self::new(base_url); + i.set_key(key); + i + } + + /// Create a new API instance with a description + /// + /// # Arguments + /// * `base_url` - base url for the API + /// * `description` - API description + /// * `description` - API examples + pub fn new_with_description(base_url: String, description: String, examples: String) -> Self { + let mut i = Self::new(base_url); + i.set_description(description); + i.set_examples(examples); + i + } + + /// Return the base url + pub fn base_url(&self) -> &String { + &self.base_url + } + + /// Set the API key credential for the API + /// + /// # Arguments + /// * `key` - API key + pub fn set_key(&mut self, key: String) -> &Self { + self.key = Some(key); + self + } + + /// Return the examples + pub fn examples(&self) -> &String { + &self.examples + } + + /// Set the examples for the API + /// + /// # Arguments + /// * `examples` - API examples + pub fn set_examples(&mut self, examples: String) -> &Self { + self.examples = examples; + self + } + + /// Return the description + pub fn description(&self) -> &String { + &self.description + } + + /// Set the description for the API + /// + /// # Arguments + /// * `description` - API description + pub fn set_description(&mut self, description: String) -> &Self { + self.description = description; + self + } + + /// Return the API key + pub fn key(&self) -> &Option { + &self.key + } + + /// Make a request to the API + /// + /// # Arguments + /// * `endpoint` - Endpoint to call + /// * `body` - Supply a body for POST, or None for GET + /// * `headers` - Vec of extra headers to supply to the API + pub fn request( + &self, + endpoint: &str, + body: Option, + headers: HashMap, + ) -> Result { + let url = format!("{}/{}", self.base_url(), endpoint); + request(&url, body, headers) + } +} + +impl fmt::Display for ApiInstance { + // This trait requires `fmt` with this exact signature. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let description = format!( + "{}{}", + if self.description().is_empty() { + "" + } else { + "\n\t" + }, + self.description() + ); + let examples = format!( + "{}{}", + if self.examples().is_empty() { + "" + } else { + "\n\t" + }, + self.examples() + ); + + write!(f, "{}{}{}", self.base_url(), description, examples) + } +} diff --git a/src/network/utils.rs b/src/network/utils.rs index a85d72b..52e4e36 100644 --- a/src/network/utils.rs +++ b/src/network/utils.rs @@ -1,53 +1,58 @@ -use crate::value::{Value}; - -use std::collections::HashMap; -use std::net::ToSocketAddrs; -use std::time::Duration; - -/// Resolve a hostname to an IP address -/// -/// # Arguments -/// * `hostname` - Host to resolve -pub fn resolve(hostname: &str) -> Result { - match (hostname, 0).to_socket_addrs() { - Ok(mut addresses) => { - let address = addresses.next().unwrap().to_string(); - let suffix = ":".to_string() + address.split(':').last().unwrap_or("80"); - - Ok(Value::String(address.replace(&suffix, ""))) - }, - Err(e) => Err(e) - } -} - -/// Fetch from a given URL -/// -/// # Arguments -/// * `url` - Target URL -/// * `body` - Body if POST -/// * `headers` - Array of header=value strings -pub fn request(url: &str, body: Option, headers: HashMap) -> Result { - match reqwest::blocking::Client::builder().timeout(Duration::from_millis(1500)).build() { - Ok(client) => { - let mut request = match body { - None => client.get(url), - Some(s) => client.post(url).body(s) - }; - - for (header, value) in headers.iter() { - request = request.header(header, value); - } - - match request.send() { - Ok(res) => { - match res.text() { - Ok(s) => Ok(Value::String(s)), - Err(e) => Err(e) - } - }, - Err(e) => Err(e) - } - }, - Err(e) => Err(e) - } -} \ No newline at end of file +use crate::value::Value; + +use std::collections::HashMap; +use std::net::ToSocketAddrs; +use std::time::Duration; + +/// Resolve a hostname to an IP address +/// +/// # Arguments +/// * `hostname` - Host to resolve +pub fn resolve(hostname: &str) -> Result { + match (hostname, 0).to_socket_addrs() { + Ok(mut addresses) => { + let address = addresses.next().unwrap().to_string(); + let suffix = ":".to_string() + address.split(':').last().unwrap_or("80"); + + Ok(Value::String(address.replace(&suffix, ""))) + } + Err(e) => Err(e), + } +} + +/// Fetch from a given URL +/// +/// # Arguments +/// * `url` - Target URL +/// * `body` - Body if POST +/// * `headers` - Array of header=value strings +pub fn request( + url: &str, + body: Option, + headers: HashMap, +) -> Result { + match reqwest::blocking::Client::builder() + .timeout(Duration::from_millis(1500)) + .build() + { + Ok(client) => { + let mut request = match body { + None => client.get(url), + Some(s) => client.post(url).body(s), + }; + + for (header, value) in headers.iter() { + request = request.header(header, value); + } + + match request.send() { + Ok(res) => match res.text() { + Ok(s) => Ok(Value::String(s)), + Err(e) => Err(e), + }, + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } +}