Skip to content

Commit a3b939c

Browse files
authored
Json improvements (#15)
* add support for json to lisp and vice versa in the web endpoint * fix some bugs with lisp/json parsing
1 parent fc34228 commit a3b939c

File tree

4 files changed

+264
-75
lines changed

4 files changed

+264
-75
lines changed

src/evaluator.rs

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ pub fn obj2str(sc: *mut s7_scheme, obj: *mut s7_cell) -> String {
8989
}
9090
}
9191

92-
pub fn scheme2json(expression: &str) -> Result<Value, String> {
92+
pub fn lisp2json(expression: &str) -> Result<Value, String> {
9393
// <TYPE>: <JSON>
9494
// ------------------------------------------------------
9595
// symbol: "string"
@@ -106,12 +106,31 @@ pub fn scheme2json(expression: &str) -> Result<Value, String> {
106106
// - @byte-vector: {"*type/byte-vector*: "deadbeef0000" }
107107
// - @float-vector: {"*type/float-vector*": [3.2, 8.6, 0.1]}
108108
// - @hash-table: {"*type/hash-table*": [["a", 6], [53, 199]]}
109+
// - @quoted: {"*type/quoted*": [["a", 6], [53, 199]]}
110+
111+
let mut owned_expr = None;
112+
let expr = {
113+
let trimmed = expression.trim_start();
114+
if let Some(rest) = trimmed.strip_prefix('\'') {
115+
let rest = rest.trim_start();
116+
if rest.is_empty() {
117+
return Err("Empty quoted expression".to_string());
118+
}
119+
let mut wrapped = String::from("(quote ");
120+
wrapped.push_str(rest);
121+
wrapped.push(')');
122+
owned_expr = Some(wrapped);
123+
owned_expr.as_deref().expect("quote wrapper missing")
124+
} else {
125+
expression
126+
}
127+
};
109128

110129
unsafe {
111130
let sc: *mut s7_scheme = s7_init();
112131

113132
// Parse the expression without evaluating it
114-
let c_expr = CString::new(expression).unwrap_or_else(|_| CString::new("()").unwrap());
133+
let c_expr = CString::new(expr).unwrap_or_else(|_| CString::new("()").unwrap());
115134
let input_port = s7_open_input_string(sc, c_expr.as_ptr());
116135
let s7_obj = s7_read(sc, input_port);
117136
s7_close_input_port(sc, input_port);
@@ -122,7 +141,7 @@ pub fn scheme2json(expression: &str) -> Result<Value, String> {
122141
}
123142
}
124143

125-
pub fn json2scheme(expression: Value) -> Result<String, String> {
144+
pub fn json2lisp(expression: &Value) -> Result<String, String> {
126145
unsafe {
127146
let sc: *mut s7_scheme = s7_init();
128147
match json_to_s7_obj(sc, &expression) {
@@ -530,21 +549,37 @@ unsafe fn s7_obj_to_json(sc: *mut s7_scheme, obj: s7_pointer) -> Result<Value, S
530549
} else if s7_is_pair(obj) {
531550
// Check if it's a quote form first
532551
let car = s7_car(obj);
552+
if s7_is_syntax(car) {
553+
let car_str = obj2str(sc, car);
554+
if car_str == "#_quote" {
555+
let cdr = s7_cdr(obj);
556+
if s7_is_pair(cdr) && s7_is_null(sc, s7_cdr(cdr)) {
557+
let quoted_expr = s7_car(cdr);
558+
let mut special_type = Map::new();
559+
special_type.insert(
560+
"*type/quoted*".to_string(),
561+
s7_obj_to_json(sc, quoted_expr)?,
562+
);
563+
return Ok(Value::Object(special_type));
564+
}
565+
}
566+
}
533567
if s7_is_symbol(car) {
534568
let symbol_name_ptr = s7_symbol_name(car);
535569
if !symbol_name_ptr.is_null() {
536570
let symbol_name = CStr::from_ptr(symbol_name_ptr).to_string_lossy();
537571

538572
if symbol_name == "quote" {
539-
// Handle quote form: convert (quote expr) to ["quote", expr]
573+
// Handle quote form: convert (quote expr) to {"*type/quoted*": <expr>}
540574
let cdr = s7_cdr(obj);
541575
if s7_is_pair(cdr) && s7_is_null(sc, s7_cdr(cdr)) {
542-
// It's a proper quote form: (quote expr)
543576
let quoted_expr = s7_car(cdr);
544-
let mut array = Vec::new();
545-
array.push(Value::String("quote".to_string()));
546-
array.push(s7_obj_to_json(sc, quoted_expr)?);
547-
return Ok(Value::Array(array));
577+
let mut special_type = Map::new();
578+
special_type.insert(
579+
"*type/quoted*".to_string(),
580+
s7_obj_to_json(sc, quoted_expr)?,
581+
);
582+
return Ok(Value::Object(special_type));
548583
}
549584
}
550585
}
@@ -639,6 +674,31 @@ unsafe fn s7_obj_to_json(sc: *mut s7_scheme, obj: s7_pointer) -> Result<Value, S
639674
// This is a simplified approach - we'd need to iterate through the hash table
640675
special_type.insert("*type/hash-table*".to_string(), Value::Array(pairs));
641676
Ok(Value::Object(special_type))
677+
} else if s7_is_syntax(obj) {
678+
// Fallback for syntax objects (e.g., nested quote shorthand).
679+
let expr = obj2str(sc, obj);
680+
let trimmed = expr.trim_start();
681+
let quoted_inner = if let Some(rest) = trimmed.strip_prefix('\'') {
682+
let rest = rest.trim_start();
683+
if rest.is_empty() {
684+
return Err("Empty quoted expression".to_string());
685+
}
686+
rest
687+
} else if let Some(rest) = trimmed.strip_prefix("(quote ") {
688+
let rest = rest.trim_end();
689+
if let Some(rest) = rest.strip_suffix(')') {
690+
rest.trim()
691+
} else {
692+
return Err("Malformed quote syntax".to_string());
693+
}
694+
} else {
695+
return Err(format!("Unsupported syntax object: {}", expr));
696+
};
697+
698+
let quoted_json = lisp2json(quoted_inner)?;
699+
let mut special_type = Map::new();
700+
special_type.insert("*type/quoted*".to_string(), quoted_json);
701+
Ok(Value::Object(special_type))
642702
} else {
643703
// For debugging: let's see what type this actually is
644704
let type_info = if s7_is_procedure(obj) {
@@ -713,6 +773,12 @@ unsafe fn json_to_s7_obj(sc: *mut s7_scheme, json: &Value) -> Result<s7_pointer,
713773
}
714774
return Ok(vector);
715775
}
776+
if let Some(value) = obj.get("*type/quoted*") {
777+
let quoted = json_to_s7_obj(sc, value)?;
778+
let quote_sym = s7_make_symbol(sc, c"quote".as_ptr());
779+
let quoted_list = s7_cons(sc, quoted, s7_nil(sc));
780+
return Ok(s7_cons(sc, quote_sym, quoted_list));
781+
}
716782
if let Some(Value::String(hex)) = obj.get("*type/byte-vector*") {
717783
let bytes: Result<Vec<u8>, ParseIntError> = (0..hex.len())
718784
.step_by(2)

src/lib.rs

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
22

33
pub use crate::config::Config;
4-
use crate::evaluator::{Evaluator, Primitive, Type, json2scheme, obj2str, scheme2json};
4+
use crate::evaluator::{Evaluator, Primitive, Type, json2lisp, obj2str, lisp2json};
55
use crate::extensions::crypto::{
66
primitive_s7_crypto_generate, primitive_s7_crypto_sign, primitive_s7_crypto_verify,
77
};
@@ -139,20 +139,83 @@ impl Journal {
139139
}
140140

141141
pub fn evaluate_json(&self, query: Value) -> Value {
142-
match json2scheme(query) {
142+
match json2lisp(&query) {
143143
Ok(scheme_query) => {
144144
let result = self.evaluate_record(NULL, scheme_query.as_str());
145-
match scheme2json(result.as_str()) {
145+
match lisp2json(result.as_str()) {
146146
Ok(json_result) => json_result,
147-
Err(_) => scheme2json("(error parse-error \"Failed to parse Scheme to JSON\")")
147+
Err(_) => {
148+
log::warn!(
149+
"Failed to parse Scheme to JSON. Result: {}",
150+
result
151+
);
152+
lisp2json("(error 'parse-error \"Failed to parse Scheme to JSON\")")
153+
}
148154
.expect("Error parsing the JSON error message"),
149155
}
150156
}
151-
Err(_) => scheme2json("(error parse-error \"Failed to parse JSON to Scheme\")")
157+
Err(_) => {
158+
let query_str = serde_json::to_string(&query)
159+
.unwrap_or_else(|_| "<unprintable json>".to_string());
160+
log::warn!(
161+
"Failed to parse JSON to Scheme. Query: {}",
162+
query_str
163+
);
164+
lisp2json("(error 'parse-error \"Failed to parse JSON to Scheme\")")
165+
}
152166
.expect("Error parsing the JSON error message"),
153167
}
154168
}
155169

170+
/// Convert a Lisp expression into its JSON representation without evaluation.
171+
///
172+
/// # Examples
173+
/// ```
174+
/// use journal_sdk::JOURNAL;
175+
/// use serde_json::json;
176+
///
177+
/// let output = JOURNAL.lisp_to_json("(+ 1 2)");
178+
/// assert_eq!(output, json!(["+", 1, 2]));
179+
/// ```
180+
pub fn lisp_to_json(&self, query: &str) -> Value {
181+
match lisp2json(query) {
182+
Ok(json_result) => json_result,
183+
Err(_) => {
184+
log::warn!(
185+
"Failed to parse Scheme to JSON. Query: {}",
186+
query
187+
);
188+
lisp2json("(error 'parse-error \"Failed to parse Scheme to JSON\")")
189+
}
190+
.expect("Error parsing the JSON error message"),
191+
}
192+
}
193+
194+
/// Convert a JSON expression into its Lisp representation without evaluation.
195+
///
196+
/// # Examples
197+
/// ```
198+
/// use journal_sdk::JOURNAL;
199+
/// use serde_json::json;
200+
///
201+
/// let output = JOURNAL.json_to_lisp(json!(["+", 1, 2]));
202+
/// assert_eq!(output, "(+ 1 2)");
203+
/// ```
204+
pub fn json_to_lisp(&self, query: Value) -> String {
205+
match json2lisp(&query) {
206+
Ok(scheme_result) => scheme_result,
207+
Err(_) => {
208+
let query_str = serde_json::to_string(&query)
209+
.unwrap_or_else(|_| "<unprintable json>".to_string());
210+
log::warn!(
211+
"Failed to parse JSON to Scheme. Query: {}",
212+
query_str
213+
);
214+
"(error 'parse-error \"Failed to parse JSON to Scheme\")".to_string()
215+
}
216+
}
217+
}
218+
156219
fn evaluate_record(&self, record: Word, query: &str) -> String {
157220
let mut runs = 0;
158221
let cache = Arc::new(Mutex::new(HashMap::new()));

src/main.rs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use journal_sdk::{Config, JOURNAL};
22
use log::info;
33
use rocket::config::Config as RocketConfig;
44
use rocket::data::{Limits, ToByteUnit};
5-
use rocket::response::content::RawHtml;
5+
use rocket::response::content::{RawHtml, RawText};
66
use rocket::serde::json::Json;
77
use rocket::{get, post, routes};
88
use serde_json::Value;
@@ -20,6 +20,8 @@ const INDEX_HTML: &str = r#"<!DOCTYPE html>
2020
<ul>
2121
<li><a href="/interface">LISP Interface</a></li>
2222
<li><a href="/interface/json">JSON Interface</a></li>
23+
<li><a href="/interface/lisp-to-json">LISP to JSON</a></li>
24+
<li><a href="/interface/json-to-lisp">JSON to LISP</a></li>
2325
</ul>
2426
</body>
2527
</html>
@@ -28,7 +30,7 @@ const INDEX_HTML: &str = r#"<!DOCTYPE html>
2830
const INTERFACE_HTML: &str = r#"<!DOCTYPE html>
2931
<html>
3032
<head>
31-
<h2>{}</h2>
33+
<h2>__TITLE__</h2>
3234
</head>
3335
<body style="padding: 0 20px; font-family: 'Consolas'">
3436
<textarea id="query" rows="8" cols="128" spellcheck="false"></textarea>
@@ -43,9 +45,7 @@ const INTERFACE_HTML: &str = r#"<!DOCTYPE html>
4345
let query = document.getElementById('query').value;
4446
fetch('', {
4547
method: 'POST',
46-
headers: {
47-
'Content-Type': 'application/json',
48-
},
48+
__HEADERS__
4949
body: query,
5050
}).then(response => {
5151
return response.text();
@@ -76,17 +76,43 @@ async fn index() -> RawHtml<String> {
7676

7777
#[get("/interface", format = "text/html")]
7878
async fn inform_lisp() -> RawHtml<String> {
79-
RawHtml(INTERFACE_HTML.replace("{}", "LISP Interface"))
79+
RawHtml(
80+
INTERFACE_HTML
81+
.replace("__TITLE__", "LISP Interface")
82+
.replace("__HEADERS__", ""),
83+
)
8084
}
8185

8286
#[post("/interface", data = "<query>", rank = 1)]
8387
async fn evaluate_lisp(query: &str) -> String {
8488
JOURNAL.evaluate(query)
8589
}
8690

91+
#[get("/interface/lisp-to-json", format = "text/html")]
92+
async fn inform_lisp_to_json() -> RawHtml<String> {
93+
RawHtml(
94+
INTERFACE_HTML
95+
.replace("__TITLE__", "LISP to JSON")
96+
.replace("__HEADERS__", ""),
97+
)
98+
}
99+
100+
#[post("/interface/lisp-to-json", data = "<query>", rank = 1)]
101+
async fn lisp_to_json(query: &str) -> Json<Value> {
102+
let result = JOURNAL.lisp_to_json(query);
103+
Json(result)
104+
}
105+
87106
#[get("/interface/json", format = "text/html")]
88107
async fn inform_json() -> RawHtml<String> {
89-
RawHtml(INTERFACE_HTML.replace("{}", "JSON Interface"))
108+
RawHtml(
109+
INTERFACE_HTML
110+
.replace("__TITLE__", "JSON Interface")
111+
.replace(
112+
"__HEADERS__",
113+
"headers: { 'Content-Type': 'application/json' },",
114+
),
115+
)
90116
}
91117

92118
#[post("/interface/json", data = "<query>", format = "json", rank = 1)]
@@ -95,6 +121,19 @@ async fn evaluate_json(query: Json<Value>) -> Json<Value> {
95121
Json(result)
96122
}
97123

124+
#[get("/interface/json-to-lisp", format = "text/html")]
125+
async fn inform_json_to_lisp() -> RawHtml<String> {
126+
RawHtml(INTERFACE_HTML.replace("__TITLE__", "JSON to LISP").replace(
127+
"__HEADERS__",
128+
"headers: { 'Content-Type': 'application/json' },",
129+
))
130+
}
131+
132+
#[post("/interface/json-to-lisp", data = "<query>", format = "json", rank = 1)]
133+
async fn json_to_lisp(query: Json<Value>) -> RawText<String> {
134+
RawText(JOURNAL.json_to_lisp(query.into_inner()))
135+
}
136+
98137
#[rocket::main]
99138
async fn main() {
100139
let config = Config::new();
@@ -156,8 +195,12 @@ async fn main() {
156195
index,
157196
inform_lisp,
158197
evaluate_lisp,
198+
inform_lisp_to_json,
199+
lisp_to_json,
159200
inform_json,
160-
evaluate_json
201+
evaluate_json,
202+
inform_json_to_lisp,
203+
json_to_lisp
161204
],
162205
)
163206
.configure(rocket_config)

0 commit comments

Comments
 (0)