diff --git a/packages/logic-bindings/src/error.rs b/packages/logic-bindings/src/error.rs index 01cce3a9..85cfffb0 100644 --- a/packages/logic-bindings/src/error.rs +++ b/packages/logic-bindings/src/error.rs @@ -1,3 +1,4 @@ +use std::string::FromUtf8Error; use thiserror::Error; use url::ParseError; @@ -15,3 +16,30 @@ pub enum CosmwasmUriError { #[error("Malformed URI: {0}")] Malformed(String), } + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum TermParseError { + #[error("Value is not UTF-8 encoded: {0}")] + NotUtf8Value(FromUtf8Error), + + #[error("Reach unexpected EOF")] + Eof, + + #[error("Expected ',' or end of sequence and got: '{0}'")] + ExpectedSeqToken(char), + + #[error("Unexpected end of array or tuple")] + UnexpectedEndOfSeq, + + #[error("Forbidden token in value: '{0}'")] + UnexpectedValueToken(char), + + #[error("Unexpected root token: '{0}'")] + UnexpectedRootToken(char), + + #[error("Empty value in array or tuple")] + EmptyValue, + + #[error("Empty tuple")] + EmptyTuple, +} diff --git a/packages/logic-bindings/src/lib.rs b/packages/logic-bindings/src/lib.rs index 72fc0200..0bd6b3dc 100644 --- a/packages/logic-bindings/src/lib.rs +++ b/packages/logic-bindings/src/lib.rs @@ -10,9 +10,11 @@ pub mod error; mod query; +mod term_parser; pub mod uri; pub use query::{Answer, AskResponse, LogicCustomQuery, Result, Substitution, Term}; +pub use term_parser::TermValue; // Exposed for testing only // Both unit tests and integration tests are compiled to native code, so everything in here does not need to compile to Wasm. diff --git a/packages/logic-bindings/src/query.rs b/packages/logic-bindings/src/query.rs index fc3053f4..4b808a04 100644 --- a/packages/logic-bindings/src/query.rs +++ b/packages/logic-bindings/src/query.rs @@ -1,3 +1,5 @@ +use crate::error::TermParseError; +use crate::term_parser::{from_str, TermValue}; use cosmwasm_std::CustomQuery; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -46,3 +48,9 @@ pub struct Term { pub name: String, pub arguments: Vec, } + +impl Term { + pub fn parse(self) -> std::result::Result { + from_str(self.name.as_str()) + } +} diff --git a/packages/logic-bindings/src/term_parser.rs b/packages/logic-bindings/src/term_parser.rs new file mode 100644 index 00000000..9703f13f --- /dev/null +++ b/packages/logic-bindings/src/term_parser.rs @@ -0,0 +1,199 @@ +use crate::error::TermParseError; + +/// Represents a Prolog response term element which can be a tuple, an array or a string value. +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum TermValue { + Tuple(Vec), + Array(Vec), + Value(String), +} + +struct Parser<'a> { + slice: &'a [u8], + index: usize, +} + +impl<'a> Parser<'a> { + pub fn new(slice: &'a [u8]) -> Parser<'_> { + Parser { slice, index: 0 } + } + + fn peek(&mut self) -> Option { + self.slice.get(self.index).cloned() + } + + fn eat_char(&mut self) { + self.index += 1; + } + + fn parse_seq(&mut self, end_seq: u8) -> Result, TermParseError> { + let mut values: Vec = Vec::new(); + loop { + match self.peek() { + None => Err(TermParseError::Eof)?, + Some(b'[') => { + self.eat_char(); + values.push(self.parse_array()?); + } + Some(b'(') => { + self.eat_char(); + values.push(self.parse_tuple()?); + } + Some(b'\'') => { + self.eat_char(); + values.push(self.parse_escaped_value()?); + } + Some(t) if t == end_seq => { + if !values.is_empty() { + Err(TermParseError::UnexpectedEndOfSeq)? + } + self.eat_char(); + break; + } + Some(_) => values.push(self.parse_value()?), + } + match self.peek() { + Some(t) if t == end_seq => { + self.eat_char(); + break; + } + Some(b',') => { + self.eat_char(); + } + Some(t) => Err(TermParseError::ExpectedSeqToken(char::from(t)))?, + None => Err(TermParseError::Eof)?, + } + } + Ok(values) + } + + fn parse_array(&mut self) -> Result { + self.parse_seq(b']').map(|elems| TermValue::Array(elems)) + } + + fn parse_tuple(&mut self) -> Result { + self.parse_seq(b')') + .and_then(|elem: Vec| { + if elem.is_empty() { + return Err(TermParseError::EmptyTuple); + } + Ok(elem) + }) + .map(|elems| TermValue::Tuple(elems)) + } + + fn parse_value(&mut self) -> Result { + let mut bytes: Vec = Vec::new(); + loop { + match self.peek() { + Some(t) if [b'[', b'(', b'\'', b'"', b' '].contains(&t) => { + Err(TermParseError::UnexpectedValueToken(char::from(t)))? + } + Some(b) if ![b']', b')', b','].contains(&b) => { + self.eat_char(); + bytes.push(b); + } + _ => break, + } + } + + if bytes.is_empty() { + return Err(TermParseError::EmptyValue); + } + + String::from_utf8(bytes) + .map_err(|e| TermParseError::NotUtf8Value(e)) + .map(|str| TermValue::Value(str)) + } + + fn parse_escaped_value(&mut self) -> Result { + let mut bytes: Vec = Vec::new(); + loop { + match self.peek() { + Some(b'\'') => { + self.eat_char(); + break; + } + Some(b'\\') => { + self.eat_char(); + match self.peek() { + Some(b'\'') => { + self.eat_char(); + bytes.push(b'\''); + } + _ => { + bytes.push(b'\\'); + } + } + } + Some(b) => { + self.eat_char(); + bytes.push(b); + } + None => Err(TermParseError::Eof)?, + } + } + + String::from_utf8(bytes) + .map_err(|e| TermParseError::NotUtf8Value(e)) + .map(|str| TermValue::Value(str)) + } + + fn parse(&mut self) -> Result { + let mut values: Vec = Vec::new(); + loop { + match self.peek() { + Some(b'[') => { + self.eat_char(); + values.push(self.parse_array()?); + } + Some(b'(') => { + self.eat_char(); + values.push(self.parse_tuple()?); + } + Some(b'\'') => { + self.eat_char(); + values.push(self.parse_escaped_value()?); + } + Some(_) => { + values.push(self.parse_value()?); + } + _ => {} + } + + match self.peek() { + Some(b',') => { + self.eat_char(); + } + None => { + break; + } + Some(t) => Err(TermParseError::UnexpectedRootToken(char::from(t)))?, + } + } + + if values.is_empty() { + return Ok(TermValue::Value(String::default())); + } + + if values.len() == 1 { + let val = values.first().unwrap(); + return Ok(val.clone()); + } + + Ok(TermValue::Tuple(values)) + } +} + +/// Parses a Prolog response term from bytes +pub fn from_slice(v: &[u8]) -> Result { + let mut parser = Parser::new(v); + let value = parser.parse()?; + + Ok(value) +} + +/// Parses a Prolog response term from a string +pub fn from_str(s: &str) -> Result { + from_slice(s.as_bytes()) +}